Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/layout/first_hash_element_indentation.yml1
-rw-r--r--.rubocop_todo/layout/line_length.yml2
-rw-r--r--.rubocop_todo/rspec/any_instance_of.yml1
-rw-r--r--.rubocop_todo/rspec/context_wording.yml2
-rw-r--r--.rubocop_todo/rspec/instance_variable.yml1
-rw-r--r--.rubocop_todo/rspec/named_subject.yml1
-rw-r--r--.rubocop_todo/rspec/return_from_stub.yml1
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/logo.js2
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue6
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue6
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js13
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue37
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone_inline.vue (renamed from app/assets/javascripts/work_items/components/work_item_milestone.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue203
-rw-r--r--app/assets/stylesheets/page_bundles/login.scss98
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/count_helper.rb2
-rw-r--r--db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb17
-rw-r--r--db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb38
-rw-r--r--db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb38
-rw-r--r--db/schema_migrations/202401042231191
-rw-r--r--db/schema_migrations/202401080723191
-rw-r--r--db/schema_migrations/202401080725461
-rw-r--r--db/structure.sql6
-rw-r--r--doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md3
-rw-r--r--doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md3
-rw-r--r--doc/development/search/advanced_search_migration_styleguide.md47
-rw-r--r--doc/user/application_security/security_dashboard/index.md6
-rw-r--r--doc/user/project/remote_development/connect_machine.md3
-rw-r--r--doc/user/project/remote_development/index.md3
-rw-r--r--doc/user/search/advanced_search.md6
-rw-r--r--doc/user/workspace/gitlab_agent_configuration.md1
-rw-r--r--doc/user/workspace/index.md52
-rw-r--r--spec/features/merge_request/user_creates_mr_spec.rb3
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb3
-rw-r--r--spec/features/users/login_spec.rb2
-rw-r--r--spec/frontend/logo_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js26
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_inline_spec.js (renamed from spec/frontend/work_items/components/work_item_milestone_spec.js)20
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js209
-rw-r--r--spec/services/groups/create_service_spec.rb264
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb6
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb105
-rw-r--r--workhorse/go.mod2
-rw-r--r--workhorse/go.sum4
49 files changed, 910 insertions, 356 deletions
diff --git a/.rubocop_todo/layout/first_hash_element_indentation.yml b/.rubocop_todo/layout/first_hash_element_indentation.yml
index 83ce52272b7..05c0cfee47c 100644
--- a/.rubocop_todo/layout/first_hash_element_indentation.yml
+++ b/.rubocop_todo/layout/first_hash_element_indentation.yml
@@ -113,7 +113,6 @@ Layout/FirstHashElementIndentation:
- 'ee/spec/services/ee/projects/unlink_fork_service_spec.rb'
- 'ee/spec/services/external_status_checks/create_service_spec.rb'
- 'ee/spec/services/external_status_checks/destroy_service_spec.rb'
- - 'ee/spec/services/groups/create_service_spec.rb'
- 'ee/spec/services/groups/destroy_service_spec.rb'
- 'ee/spec/services/iterations/create_service_spec.rb'
- 'ee/spec/services/projects/create_service_spec.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index b8b267d6d86..1b241836da3 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -2117,7 +2117,6 @@ Layout/LineLength:
- 'ee/spec/services/geo/hashed_storage_attachments_event_store_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/check_future_renewal_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
- - 'ee/spec/services/groups/create_service_spec.rb'
- 'ee/spec/services/groups/memberships/export_service_spec.rb'
- 'ee/spec/services/groups/transfer_service_spec.rb'
- 'ee/spec/services/groups/update_repository_storage_service_spec.rb'
@@ -4462,7 +4461,6 @@ Layout/LineLength:
- 'spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb'
- 'spec/services/google_cloud/service_accounts_service_spec.rb'
- 'spec/services/groups/autocomplete_service_spec.rb'
- - 'spec/services/groups/create_service_spec.rb'
- 'spec/services/groups/transfer_service_spec.rb'
- 'spec/services/groups/update_service_spec.rb'
- 'spec/services/groups/update_shared_runners_service_spec.rb'
diff --git a/.rubocop_todo/rspec/any_instance_of.yml b/.rubocop_todo/rspec/any_instance_of.yml
index 9393170aa03..bac4094b61b 100644
--- a/.rubocop_todo/rspec/any_instance_of.yml
+++ b/.rubocop_todo/rspec/any_instance_of.yml
@@ -251,7 +251,6 @@ RSpec/AnyInstanceOf:
- 'spec/services/events/render_service_spec.rb'
- 'spec/services/git/branch_push_service_spec.rb'
- 'spec/services/git/process_ref_changes_service_spec.rb'
- - 'spec/services/groups/create_service_spec.rb'
- 'spec/services/groups/update_service_spec.rb'
- 'spec/services/issuable/destroy_service_spec.rb'
- 'spec/services/issues/close_service_spec.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index a141ac1e508..2238a9b722c 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -715,7 +715,6 @@ RSpec/ContextWording:
- 'ee/spec/services/gitlab_subscriptions/create_service_spec.rb'
- 'ee/spec/services/gitlab_subscriptions/preview_billable_user_change_service_spec.rb'
- 'ee/spec/services/group_saml/group_managed_accounts/transfer_membership_service_spec.rb'
- - 'ee/spec/services/groups/create_service_spec.rb'
- 'ee/spec/services/groups/destroy_service_spec.rb'
- 'ee/spec/services/groups/mark_for_deletion_service_spec.rb'
- 'ee/spec/services/groups/memberships/export_service_spec.rb'
@@ -2562,7 +2561,6 @@ RSpec/ContextWording:
- 'spec/services/git/wiki_push_service_spec.rb'
- 'spec/services/google_cloud/generate_pipeline_service_spec.rb'
- 'spec/services/gpg_keys/create_service_spec.rb'
- - 'spec/services/groups/create_service_spec.rb'
- 'spec/services/groups/deploy_tokens/revoke_service_spec.rb'
- 'spec/services/groups/destroy_service_spec.rb'
- 'spec/services/groups/group_links/create_service_spec.rb'
diff --git a/.rubocop_todo/rspec/instance_variable.yml b/.rubocop_todo/rspec/instance_variable.yml
index 51290ec9294..aca1a6d2d97 100644
--- a/.rubocop_todo/rspec/instance_variable.yml
+++ b/.rubocop_todo/rspec/instance_variable.yml
@@ -24,7 +24,6 @@ RSpec/InstanceVariable:
- 'ee/spec/services/ee/notification_service_spec.rb'
- 'ee/spec/services/ee/users/create_service_spec.rb'
- 'ee/spec/services/geo/metrics_update_service_spec.rb'
- - 'ee/spec/services/groups/create_service_spec.rb'
- 'ee/spec/services/groups/participants_service_spec.rb'
- 'ee/spec/services/projects/create_from_template_service_spec.rb'
- 'ee/spec/services/projects/create_service_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index eda8e34b6f4..79afdfa04f6 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -3278,7 +3278,6 @@ RSpec/NamedSubject:
- 'spec/services/gpg_keys/create_service_spec.rb'
- 'spec/services/gpg_keys/destroy_service_spec.rb'
- 'spec/services/groups/autocomplete_service_spec.rb'
- - 'spec/services/groups/create_service_spec.rb'
- 'spec/services/groups/deploy_tokens/revoke_service_spec.rb'
- 'spec/services/groups/group_links/create_service_spec.rb'
- 'spec/services/groups/group_links/destroy_service_spec.rb'
diff --git a/.rubocop_todo/rspec/return_from_stub.yml b/.rubocop_todo/rspec/return_from_stub.yml
index 897d15c72ed..5aaa1847409 100644
--- a/.rubocop_todo/rspec/return_from_stub.yml
+++ b/.rubocop_todo/rspec/return_from_stub.yml
@@ -198,7 +198,6 @@ RSpec/ReturnFromStub:
- 'spec/services/git/branch_hooks_service_spec.rb'
- 'spec/services/git/branch_push_service_spec.rb'
- 'spec/services/git/process_ref_changes_service_spec.rb'
- - 'spec/services/groups/create_service_spec.rb'
- 'spec/services/groups/nested_create_service_spec.rb'
- 'spec/services/merge_requests/merge_orchestration_service_spec.rb'
- 'spec/services/merge_requests/merge_service_spec.rb'
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 985bd590db9..34326d62e6f 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-v16.8.0-rc3
+v16.8.0
diff --git a/Gemfile b/Gemfile
index 2492f5bcdda..8f0ac1be462 100644
--- a/Gemfile
+++ b/Gemfile
@@ -516,7 +516,7 @@ group :test do
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'gitlab_quality-test_tooling', '~> 1.9.0', require: false, feature_category: :tooling
+ gem 'gitlab_quality-test_tooling', '~> 1.11.0', require: false, feature_category: :tooling
end
gem 'octokit', '~> 6.0' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index cd6c98cf406..9048638cb6c 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -221,7 +221,7 @@
{"name":"gitlab-styles","version":"11.0.0","platform":"ruby","checksum":"0dd8ec066ce9955ac51d3616c6bfded30f75bb526f39ff392ece6f43d5b9406b"},
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
-{"name":"gitlab_quality-test_tooling","version":"1.9.0","platform":"ruby","checksum":"0d47a72282e8e4d3dfc3094fe8eb2309a37c583570739a04e26afe38cd2da3a2"},
+{"name":"gitlab_quality-test_tooling","version":"1.11.0","platform":"ruby","checksum":"2146dbaddc5ffcaf4c6e876e6e9d9eb83bae25f378e0c9e5fca8d996ace3d02a"},
{"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"},
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
{"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 1c4388e3059..5386d595d52 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -728,7 +728,7 @@ GEM
omniauth (>= 1.3, < 3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
rubyntlm (~> 0.5)
- gitlab_quality-test_tooling (1.9.0)
+ gitlab_quality-test_tooling (1.11.0)
activesupport (>= 6.1, < 7.2)
amatch (~> 0.4.1)
gitlab (~> 4.19)
@@ -1909,7 +1909,7 @@ DEPENDENCIES
gitlab-utils!
gitlab_chronic_duration (~> 0.12)
gitlab_omniauth-ldap (~> 2.2.0)
- gitlab_quality-test_tooling (~> 1.9.0)
+ gitlab_quality-test_tooling (~> 1.11.0)
gon (~> 6.4.0)
google-apis-androidpublisher_v3 (~> 0.34.0)
google-apis-cloudbilling_v1 (~> 0.21.0)
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index c76e44a196d..b4933376d4e 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -13,7 +13,7 @@ export function initPortraitLogoDetection() {
const isPortrait = img.height > img.width;
if (isPortrait) {
// Limit the width when the logo has portrait format
- img.classList.replace('gl-h-9', 'gl-w-10');
+ img.classList.replace('gl-h-10', 'gl-w-10');
}
img.classList.remove('gl-visibility-hidden');
},
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index 538aa090aa8..a753641ffd2 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -42,6 +42,11 @@ export default {
required: false,
default: () => ({}),
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -120,6 +125,7 @@ export default {
:default="currentBranch"
:toggle-class="toggleClass.branch"
:data-qa-compare-side="compareSide"
+ :disabled="disabled"
data-testid="compare-dropdown"
@selected="selectBranch"
/>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index 20989206a51..35fbf4bc4e6 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: '',
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -131,6 +136,7 @@ export default {
:toggle-text="current.text || dropdownHeader"
:header-text="dropdownHeader"
:searching="isLoading"
+ :disabled="disabled"
searchable
class="gl-w-full dropdown-target-project"
:toggle-class="[
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 8cb1462c883..3d877bb3abb 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -16,8 +16,9 @@ if (mrNewCompareNode) {
const sourceCompareEl = document.getElementById('js-source-project-dropdown');
const compareEl = document.querySelector('.js-merge-request-new-compare');
const targetBranch = Vue.observable({ name: '' });
-
const currentSourceBranch = JSON.parse(sourceCompareEl.dataset.currentBranch);
+ const sourceBranch = Vue.observable(currentSourceBranch);
+
// eslint-disable-next-line no-new
new Vue({
el: sourceCompareEl,
@@ -52,6 +53,9 @@ if (mrNewCompareNode) {
if (targetBranchName) {
targetBranch.name = targetBranchName;
}
+
+ sourceBranch.value = branchName;
+ sourceBranch.text = branchName;
},
},
render(h) {
@@ -102,9 +106,14 @@ if (mrNewCompareNode) {
return currentTargetBranch;
},
+ isDisabled() {
+ return !sourceBranch.value;
+ },
},
render(h) {
- return h(CompareApp, { props: { currentBranch: this.currentBranch } });
+ return h(CompareApp, {
+ props: { currentBranch: this.currentBranch, disabled: this.isDisabled },
+ });
},
});
} else {
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index f71e6e6b405..79f0fdca061 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -17,14 +17,16 @@ import {
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
-import WorkItemMilestone from './work_item_milestone.vue';
+import WorkItemMilestoneInline from './work_item_milestone_inline.vue';
+import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue';
import WorkItemParentInline from './work_item_parent_inline.vue';
import WorkItemParent from './work_item_parent_with_edit.vue';
export default {
components: {
WorkItemLabels,
- WorkItemMilestone,
+ WorkItemMilestoneInline,
+ WorkItemMilestoneWithEdit,
WorkItemAssignees,
WorkItemDueDate,
WorkItemParent,
@@ -140,15 +142,28 @@ export default {
:work-item-type="workItemType"
@error="$emit('error', $event)"
/>
- <work-item-milestone
- v-if="workItemMilestone"
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :work-item-milestone="workItemMilestone.milestone"
- :work-item-type="workItemType"
- :can-update="canUpdate"
- @error="$emit('error', $event)"
- />
+ <template v-if="workItemMilestone">
+ <work-item-milestone-with-edit
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :can-update="canUpdate"
+ @error="$emit('error', $event)"
+ />
+ <work-item-milestone-inline
+ v-else
+ class="gl-mb-5"
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :can-update="canUpdate"
+ @error="$emit('error', $event)"
+ />
+ </template>
<template v-if="workItemWeight">
<work-item-weight
v-if="glFeatures.workItemsMvc2"
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone_inline.vue
index dbeb3d4d3ff..dbeb3d4d3ff 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone_inline.vue
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
new file mode 100644
index 00000000000..9588d21a3c5
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
@@ -0,0 +1,203 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import Tracking from '~/tracking';
+import { s__, __ } from '~/locale';
+import { MILESTONE_STATE } from '~/sidebar/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+export default {
+ i18n: {
+ milestone: s__('WorkItem|Milestone'),
+ none: s__('WorkItem|None'),
+ noMilestone: s__('WorkItem|No milestone'),
+ milestoneFetchError: s__(
+ 'WorkItem|Something went wrong while fetching milestones. Please try again.',
+ ),
+ expiredText: __('(expired)'),
+ },
+ components: {
+ WorkItemSidebarDropdownWidgetWithEdit,
+ GlLink,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemMilestone: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ shouldFetch: false,
+ updateInProgress: false,
+ milestones: [],
+ localMilestone: this.workItemMilestone,
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ emptyPlaceholder() {
+ return this.canUpdate ? this.$options.i18n.noMilestone : this.$options.i18n.none;
+ },
+ expired() {
+ return this.localMilestone?.expired ? ` ${this.$options.i18n.expiredText}` : '';
+ },
+ dropdownText() {
+ return this.localMilestone?.title
+ ? `${this.localMilestone?.title}${this.expired}`
+ : this.emptyPlaceholder;
+ },
+ isLoadingMilestones() {
+ return this.$apollo.queries.milestones.loading;
+ },
+ milestonesList() {
+ return this.milestones.map(({ id, title, expired }) => ({
+ value: id,
+ text: title,
+ expired,
+ }));
+ },
+ },
+ apollo: {
+ milestones: {
+ query: projectMilestonesQuery,
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ title: this.searchTerm,
+ state: MILESTONE_STATE.ACTIVE,
+ first: 20,
+ };
+ },
+ skip() {
+ return !this.shouldFetch;
+ },
+ update(data) {
+ return data?.workspace?.attributes?.nodes || [];
+ },
+ error() {
+ this.$emit('error', this.i18n.milestoneFetchError);
+ },
+ },
+ },
+ methods: {
+ onDropdownShown() {
+ this.searchTerm = '';
+ this.shouldFetch = true;
+ },
+ search(searchTerm) {
+ this.searchTerm = searchTerm;
+ this.shouldFetch = true;
+ },
+ itemExpiredText(item) {
+ return item.expired ? this.$options.i18n.expiredText : '';
+ },
+ updateMilestone(selectedMilestoneId) {
+ if (this.localMilestone?.id === selectedMilestoneId) {
+ return;
+ }
+
+ this.localMilestone = selectedMilestoneId
+ ? this.milestones.find(({ id }) => id === selectedMilestoneId)
+ : null;
+
+ this.track('updated_milestone');
+ this.updateInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ milestoneWidget: {
+ milestoneId: selectedMilestoneId,
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('\n'));
+ }
+ })
+ .catch((error) => {
+ this.localMilestone = this.workItemMilestone;
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.updateInProgress = false;
+ this.searchTerm = '';
+ this.shouldFetch = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <work-item-sidebar-dropdown-widget-with-edit
+ :dropdown-label="$options.i18n.milestone"
+ :can-update="canUpdate"
+ dropdown-name="milestone"
+ :loading="isLoadingMilestones"
+ :list-items="milestonesList"
+ :item-value="localMilestone"
+ :update-in-progress="updateInProgress"
+ :toggle-dropdown-text="dropdownText"
+ :header-text="__('Select milestone')"
+ :reset-button-label="__('Clear')"
+ data-testid="work-item-milestone-with-edit"
+ @dropdownShown="onDropdownShown"
+ @searchStarted="search"
+ @updateValue="updateMilestone"
+ >
+ <template #list-item="{ item }">
+ <div>{{ item.text }}{{ itemExpiredText(item) }}</div>
+ <div v-if="item.title">{{ item.title }}</div>
+ </template>
+ <template #readonly>
+ <gl-link class="gl-text-gray-900!" :href="localMilestone.webPath">
+ {{ localMilestone.title }}{{ expired }}
+ </gl-link>
+ </template>
+ </work-item-sidebar-dropdown-widget-with-edit>
+</template>
diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss
index 4cc44b01e60..bdbb2fb786f 100644
--- a/app/assets/stylesheets/page_bundles/login.scss
+++ b/app/assets/stylesheets/page_bundles/login.scss
@@ -16,43 +16,8 @@
top: 8px;
}
- .brand-holder {
- font-size: 18px;
- line-height: 1.5;
-
- p {
- font-size: 16px;
- color: $login-brand-holder-color;
- }
-
- h3 {
- font-size: 22px;
- }
-
- img {
- max-width: 100%;
- margin-bottom: 30px;
- }
-
- a {
- font-weight: $gl-font-weight-bold;
- }
- }
-
- p {
- font-size: 13px;
- }
-
- .signin-text {
- p {
- margin-bottom: 0;
- line-height: 1.5;
- }
- }
-
.borderless {
- .login-box,
- .omniauth-container {
+ .login-box {
box-shadow: none;
}
}
@@ -64,67 +29,6 @@
}
}
- .login-box,
- .omniauth-container {
- box-shadow: 0 0 0 1px $border-color;
- border-radius: $border-radius;
-
- .login-heading h3 {
- font-weight: $gl-font-weight-normal;
- line-height: 1.5;
- margin: 0 0 10px;
- }
-
- .login-footer {
- margin-top: 10px;
-
- p:last-child {
- margin-bottom: 0;
- }
- }
-
- a.forgot {
- float: right;
- padding-top: 6px;
- }
-
- .nav .active a {
- background: transparent;
- }
-
- // Styles the glowing border of focused input for username async validation
- .login-body {
- font-size: 13px;
-
- .username .validation-success {
- color: $green-600;
- }
-
- .username .validation-error {
- color: $red-500;
- }
-
- .terms .gl-form-checkbox {
- @include gl-reset-font-size;
- }
- }
- }
-
- .omniauth-container {
- border-radius: $border-radius;
- font-size: 13px;
-
- p {
- margin: 0;
- }
-
- form {
- padding: 0;
- border: 0;
- background: none;
- }
- }
-
.new-session-tabs {
&.nav-links-unboxed {
border-color: transparent;
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 07a5e711d1c..81aa4757862 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -44,7 +44,7 @@ module AppearancesHelper
end
def brand_image
- image_tag(brand_image_path, alt: brand_title, class: 'gl-visibility-hidden gl-h-9 js-portrait-logo-detection')
+ image_tag(brand_image_path, alt: brand_title, class: 'gl-visibility-hidden gl-h-10 js-portrait-logo-detection')
end
def brand_image_path
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
index 62bb2e4da23..aed730850fd 100644
--- a/app/helpers/count_helper.rb
+++ b/app/helpers/count_helper.rb
@@ -11,7 +11,7 @@ module CountHelper
# This will approximate the fork count by checking all counting all fork network
# memberships, and deducting 1 for each root of the fork network.
- # This might be inacurate as the root of the fork network might have been deleted.
+ # This might be inaccurate as the root of the fork network might have been deleted.
#
# This makes querying this information a lot more efficient and it should be
# accurate enough for the instance wide statistics
diff --git a/db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb b/db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb
new file mode 100644
index 00000000000..99a2dccc8f5
--- /dev/null
+++ b/db/post_migrate/20240104223119_add_index_owasp_top_10_with_project_id_on_vulnerability_reads.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOwaspTop10WithProjectIdOnVulnerabilityReads < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.8'
+
+ INDEX_NAME = 'index_vuln_reads_on_project_id_owasp_top_10'
+
+ def up
+ add_concurrent_index :vulnerability_reads, [:project_id, :owasp_top_10], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :vulnerability_reads, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb b/db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb
new file mode 100644
index 00000000000..23219a9d90a
--- /dev/null
+++ b/db/post_migrate/20240108072319_add_fk_to_ci_build_trace_metadata_on_partition_id_and_trace_artifact_id2.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class AddFkToCiBuildTraceMetadataOnPartitionIdAndTraceArtifactId2 < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ disable_ddl_transaction!
+
+ SOURCE_TABLE_NAME = :ci_build_trace_metadata
+ TARGET_TABLE_NAME = :ci_job_artifacts
+ COLUMN = :trace_artifact_id
+ TARGET_COLUMN = :id
+ FK_NAME = :fk_21d25cac1a_p
+ PARTITION_COLUMN = :partition_id
+
+ def up
+ add_concurrent_foreign_key(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ column: [PARTITION_COLUMN, COLUMN],
+ target_column: [PARTITION_COLUMN, TARGET_COLUMN],
+ validate: true,
+ reverse_lock_order: true,
+ on_update: :cascade,
+ on_delete: :cascade,
+ name: FK_NAME
+ )
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ name: FK_NAME,
+ reverse_lock_order: true
+ )
+ end
+ end
+end
diff --git a/db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb b/db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb
new file mode 100644
index 00000000000..9b74f7019b9
--- /dev/null
+++ b/db/post_migrate/20240108072546_add_fk_to_ci_job_artifact_states_on_partition_id_and_job_artifact_id2.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class AddFkToCiJobArtifactStatesOnPartitionIdAndJobArtifactId2 < Gitlab::Database::Migration[2.2]
+ milestone '16.8'
+ disable_ddl_transaction!
+
+ SOURCE_TABLE_NAME = :ci_job_artifact_states
+ TARGET_TABLE_NAME = :ci_job_artifacts
+ COLUMN = :job_artifact_id
+ TARGET_COLUMN = :id
+ FK_NAME = :fk_rails_80a9cba3b2_p
+ PARTITION_COLUMN = :partition_id
+
+ def up
+ add_concurrent_foreign_key(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ column: [PARTITION_COLUMN, COLUMN],
+ target_column: [PARTITION_COLUMN, TARGET_COLUMN],
+ validate: true,
+ reverse_lock_order: true,
+ on_update: :cascade,
+ on_delete: :cascade,
+ name: FK_NAME
+ )
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ name: FK_NAME,
+ reverse_lock_order: true
+ )
+ end
+ end
+end
diff --git a/db/schema_migrations/20240104223119 b/db/schema_migrations/20240104223119
new file mode 100644
index 00000000000..af271c3ce80
--- /dev/null
+++ b/db/schema_migrations/20240104223119
@@ -0,0 +1 @@
+835c483008b589033f825535c381b963d5c20b2aa00f849376e05b69864f68ff \ No newline at end of file
diff --git a/db/schema_migrations/20240108072319 b/db/schema_migrations/20240108072319
new file mode 100644
index 00000000000..8f82a6baf77
--- /dev/null
+++ b/db/schema_migrations/20240108072319
@@ -0,0 +1 @@
+67af5f06d58f67d7ce0f27c8fd5eb0856772b98443a59cd077b076494fed6634 \ No newline at end of file
diff --git a/db/schema_migrations/20240108072546 b/db/schema_migrations/20240108072546
new file mode 100644
index 00000000000..997e37dba92
--- /dev/null
+++ b/db/schema_migrations/20240108072546
@@ -0,0 +1 @@
+981e263b0c9715324d86ed29534465ebcf2d37c8bb9e5dc0d93b9abf11f264d4 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c444798f901..82417b9b6be 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -35671,6 +35671,8 @@ CREATE INDEX index_vuln_reads_on_casted_cluster_agent_id_where_it_is_null ON vul
CREATE INDEX index_vuln_reads_on_namespace_id_state_severity_and_vuln_id ON vulnerability_reads USING btree (namespace_id, state, severity, vulnerability_id DESC);
+CREATE INDEX index_vuln_reads_on_project_id_owasp_top_10 ON vulnerability_reads USING btree (project_id, owasp_top_10);
+
CREATE INDEX index_vuln_reads_on_project_id_state_severity_and_vuln_id ON vulnerability_reads USING btree (project_id, state, severity, vulnerability_id DESC);
CREATE INDEX index_vulnerabilities_common_finder_query_on_default_branch ON vulnerabilities USING btree (project_id, state, report_type, present_on_default_branch, severity, id);
@@ -38121,7 +38123,7 @@ ALTER TABLE ONLY ci_build_trace_metadata
ADD CONSTRAINT fk_21d25cac1a FOREIGN KEY (trace_artifact_id) REFERENCES ci_job_artifacts(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_build_trace_metadata
- ADD CONSTRAINT fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
+ ADD CONSTRAINT fk_21d25cac1a_p FOREIGN KEY (partition_id, trace_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY users_star_projects
ADD CONSTRAINT fk_22cd27ddfc FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@@ -40062,7 +40064,7 @@ ALTER TABLE ONLY ci_job_artifact_states
ADD CONSTRAINT fk_rails_80a9cba3b2 FOREIGN KEY (job_artifact_id) REFERENCES ci_job_artifacts(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_job_artifact_states
- ADD CONSTRAINT fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
+ ADD CONSTRAINT fk_rails_80a9cba3b2_p FOREIGN KEY (partition_id, job_artifact_id) REFERENCES ci_job_artifacts(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY approval_merge_request_rules_users
ADD CONSTRAINT fk_rails_80e6801803 FOREIGN KEY (approval_merge_request_rule_id) REFERENCES approval_merge_request_rules(id) ON DELETE CASCADE;
diff --git a/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md b/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md
index fa149e4a3dc..699a41879a9 100644
--- a/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md
+++ b/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md
@@ -2,8 +2,11 @@
stage: enablement
group: Tenant Scale
description: 'Cells Stateless Router Proposal'
+status: rejected
---
+_This proposal was superseded by the [routing service proposal](routing-service.md)_
+
<!-- vale gitlab.FutureTense = NO -->
This document is a work-in-progress and represents a very early state of the
diff --git a/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md b/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md
index 2978936bf4e..72b96e9ab8c 100644
--- a/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md
+++ b/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md
@@ -2,8 +2,11 @@
stage: enablement
group: Tenant Scale
description: 'Cells Stateless Router Proposal'
+status: rejected
---
+_This proposal was superseded by the [routing service proposal](routing-service.md)_
+
<!-- vale gitlab.FutureTense = NO -->
This document is a work-in-progress and represents a very early state of the
diff --git a/doc/development/search/advanced_search_migration_styleguide.md b/doc/development/search/advanced_search_migration_styleguide.md
index 87083d1a36f..b79ab6561d5 100644
--- a/doc/development/search/advanced_search_migration_styleguide.md
+++ b/doc/development/search/advanced_search_migration_styleguide.md
@@ -225,6 +225,53 @@ class MigrationName < Elastic::Migration
end
```
+#### `Search::Elastic::MigrationDeleteBasedOnSchemaVersion`
+
+Deletes all documents in the index that stores the specified document type and has `schema_version` less than the given value.
+
+Requires the `DOCUMENT_TYPE` constant and `schema_version` method.
+The index mapping must have a `schema_version` integer field in a `YYMM` format.
+
+```ruby
+class MigrationName < Elastic::Migration
+ include ::Search::Elastic::MigrationDeleteBasedOnSchemaVersion
+
+ DOCUMENT_TYPE = Issue
+
+ batch_size 10_000
+ batched!
+ throttle_delay 1.minute
+ retry_on_failure
+
+ def schema_version
+ 23_12
+ end
+end
+```
+
+#### `Search::Elastic::MigrationDatabaseBackfillHelper`
+
+Reindexes all documents in the database to the elastic search index respecting the `limited_indexing` setting.
+
+Requires the `DOCUMENT_TYPE` constant and `respect_limited_indexing?` method.
+
+```ruby
+class MigrationName < Elastic::Migration
+ include ::Search::Elastic::MigrationDatabaseBackfillHelper
+
+ batch_size 10_000
+ batched!
+ throttle_delay 1.minute
+ retry_on_failure
+
+ DOCUMENT_TYPE = Issue
+
+ def respect_limited_indexing?
+ true
+ end
+end
+```
+
#### `Elastic::MigrationHelper`
Contains methods you can use when a migration doesn't fit the previous examples.
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 405017ab023..e9f3a3a2c0b 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -20,6 +20,12 @@ The data provided by the Security Dashboards can be used supply to insight on wh
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Security Dashboard](https://www.youtube.com/watch?v=Uo-pDns1OpQ).
+## Vulnerability metrics in the Value Streams Dashboard
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383697) in GitLab 16.0.
+
+You can view vulnerability metrics also in the [Value Streams Dashboard](../../../user/analytics/value_streams_dashboard.md) comparison panel, which helps you understand security exposure in the context of your organization's software delivery workflows.
+
## Prerequisites
To view the Security Dashboards, the following is required:
diff --git a/doc/user/project/remote_development/connect_machine.md b/doc/user/project/remote_development/connect_machine.md
index df2921b184c..b37a2c5fc0f 100644
--- a/doc/user/project/remote_development/connect_machine.md
+++ b/doc/user/project/remote_development/connect_machine.md
@@ -13,9 +13,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com, this feature is available. The feature is not ready for production use.
-WARNING:
-This feature is in [Beta](../../../policy/experiment-beta-support.md#beta) and subject to change without notice.
-
This tutorial shows you how to:
- Create a development environment outside of GitLab.
diff --git a/doc/user/project/remote_development/index.md b/doc/user/project/remote_development/index.md
index c11d8591745..65445e54949 100644
--- a/doc/user/project/remote_development/index.md
+++ b/doc/user/project/remote_development/index.md
@@ -13,9 +13,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `vscode_web_ide`. On GitLab.com, this feature is available. The feature is not ready for production use.
-WARNING:
-This feature is in [Beta](../../../policy/experiment-beta-support.md#beta) and subject to change without notice.
-
You can use remote development to write and compile code hosted on GitLab.
With remote development, you can:
diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md
index 3b715fb13da..757231ffb80 100644
--- a/doc/user/search/advanced_search.md
+++ b/doc/user/search/advanced_search.md
@@ -30,10 +30,10 @@ You can use advanced search in:
## Enable advanced search
-- On GitLab.com, advanced search is enabled for groups with paid subscriptions.
-- For self-managed GitLab instances, an administrator must
+- For [GitLab SaaS](../../subscriptions/gitlab_com/index.md) and [GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md),
+ advanced search is enabled in paid subscriptions.
+- For [GitLab self-managed](../../subscriptions/self_managed/index.md), an administrator must
[enable advanced search](../../integration/advanced_search/elasticsearch.md#enable-advanced-search).
-- For GitLab Dedicated, advanced search is enabled.
## Syntax
diff --git a/doc/user/workspace/gitlab_agent_configuration.md b/doc/user/workspace/gitlab_agent_configuration.md
index 0e35c72c5ef..bf48521b612 100644
--- a/doc/user/workspace/gitlab_agent_configuration.md
+++ b/doc/user/workspace/gitlab_agent_configuration.md
@@ -85,6 +85,7 @@ The default value is:
```yaml
remote_development:
network_policy:
+ enabled: true
egress:
- allow: "0.0.0.0/0"
except:
diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md
index 309c5d19a51..5fa6108de6f 100644
--- a/doc/user/workspace/index.md
+++ b/doc/user/workspace/index.md
@@ -67,32 +67,37 @@ The devfile is used to automatically configure the development environment with
This way, you can create consistent and reproducible development environments
regardless of the machine or platform you use.
-### Relevant schema properties
-
-GitLab only supports the `container` and `volume` components in [devfile 2.2.0](https://devfile.io/docs/2.2.0/devfile-schema).
-Use the `container` component to define a container image as the execution environment for a devfile workspace.
+### Validation rules
+
+- `schemaVersion` must be [`2.2.0`](https://devfile.io/docs/2.2.0/devfile-schema).
+- The devfile must have at least one component.
+- For `components`:
+ - Names must not start with `gl-`.
+ - Only [`container`](#container-component-type) and `volume` are supported.
+- For `commands`, IDs must not start with `gl-`.
+- For `events`:
+ - Names must not start with `gl-`.
+ - Only `preStart` is supported.
+- `parent`, `projects`, and `starterProjects` are not supported.
+- For `variables`, keys must not start with `gl-`, `gl_`, `GL-`, or `GL_`.
+
+### `container` component type
+
+Use the `container` component type to define a container image as the execution environment for a workspace.
You can specify the base image, dependencies, and other settings.
-Only these properties are relevant to the GitLab implementation of the `container` component:
-
-| Properties | Definition |
-|----------------| ----------------------------------------------------------------------------------|
-| `image` | Name of the container image to use for the workspace. |
-| `memoryRequest`| Minimum amount of memory the container can use. |
-| `memoryLimit` | Maximum amount of memory the container can use. |
-| `cpuRequest` | Minimum amount of CPU the container can use. |
-| `cpuLimit` | Maximum amount of CPU the container can use. |
-| `env` | Environment variables to use in the container. |
-| `endpoints` | Port mappings to expose from the container. |
-| `volumeMounts` | Storage volume to mount in the container. |
-
-### Using variables in a devfile
-
-You can define variables to use in your devfile.
-The `variables` object is a map of name-value pairs that you can use for string replacement in the devfile.
+The `container` component type supports the following schema properties only:
-Variables cannot have names that start with `gl-`, `gl_`, `GL-`, or `GL_`.
-For more information about how and where to use variables, see the [devfile documentation](https://devfile.io/docs/2.2.0/defining-variables).
+| Property | Description |
+|----------------| -------------------------------------------------------------------------------------------------------------------------------|
+| `image` | Name of the container image to use for the workspace. |
+| `memoryRequest`| Minimum amount of memory the container can use. |
+| `memoryLimit` | Maximum amount of memory the container can use. |
+| `cpuRequest` | Minimum amount of CPU the container can use. |
+| `cpuLimit` | Maximum amount of CPU the container can use. |
+| `env` | Environment variables to use in the container. Names must not start with `gl-`. |
+| `endpoints` | Port mappings to expose from the container. Names must not start with `gl-`. |
+| `volumeMounts` | Storage volume to mount in the container. |
### Example configurations
@@ -181,4 +186,3 @@ For more information, see the
## Related topics
- [GitLab workspaces demo](https://go.gitlab.com/qtu66q)
-- [Developer documentation](../../development/remote_development/index.md)]
diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb
index 950b64bb395..5f6e465d011 100644
--- a/spec/features/merge_request/user_creates_mr_spec.rb
+++ b/spec/features/merge_request/user_creates_mr_spec.rb
@@ -74,6 +74,9 @@ RSpec.describe 'Merge request > User creates MR', feature_category: :code_review
visit project_new_merge_request_path(source_project)
+ find('.js-source-branch').click
+ select_listbox_item('master')
+
first('.js-target-project').click
select_listbox_item(target_project.full_path)
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index daa84227adc..a68bdfd7356 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -43,6 +43,9 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
+ find('.js-source-branch').click
+ select_listbox_item('master')
+
first('.js-target-branch').click
find('.gl-listbox-search-input').native.send_keys 'v1.1.0'
select_listbox_item('v1.1.0')
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 87ed4ced684..c5ad7bca824 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -789,7 +789,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
visit new_user_session_path
image = find('img.js-portrait-logo-detection')
- expect(image['class']).to include('gl-h-9')
+ expect(image['class']).to include('gl-h-10')
end
it 'renders link to sign up path' do
diff --git a/spec/frontend/logo_spec.js b/spec/frontend/logo_spec.js
index 8e39e75bd3b..51f47fb89ba 100644
--- a/spec/frontend/logo_spec.js
+++ b/spec/frontend/logo_spec.js
@@ -10,7 +10,7 @@ describe('initPortraitLogoDetection', () => {
};
beforeEach(() => {
- setHTMLFixture('<img class="gl-visibility-hidden gl-h-9 js-portrait-logo-detection" />');
+ setHTMLFixture('<img class="gl-visibility-hidden gl-h-10 js-portrait-logo-detection" />');
initPortraitLogoDetection();
img = document.querySelector('img');
});
@@ -27,12 +27,12 @@ describe('initPortraitLogoDetection', () => {
it('removes gl-visibility-hidden', () => {
expect(img.classList).toContain('gl-visibility-hidden');
- expect(img.classList).toContain('gl-h-9');
+ expect(img.classList).toContain('gl-h-10');
loadImage();
expect(img.classList).not.toContain('gl-visibility-hidden');
- expect(img.classList).toContain('gl-h-9');
+ expect(img.classList).toContain('gl-h-10');
});
});
@@ -44,7 +44,7 @@ describe('initPortraitLogoDetection', () => {
it('removes gl-visibility-hidden', () => {
expect(img.classList).toContain('gl-visibility-hidden');
- expect(img.classList).toContain('gl-h-9');
+ expect(img.classList).toContain('gl-h-10');
loadImage();
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
index 48ec84ceb85..43f7027406f 100644
--- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -3,7 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
-import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+import WorkItemMilestoneInline from '~/work_items/components/work_item_milestone_inline.vue';
+import WorkItemMilestoneWithEdit from '~/work_items/components/work_item_milestone_with_edit.vue';
import WorkItemParentInline from '~/work_items/components/work_item_parent_inline.vue';
import WorkItemParent from '~/work_items/components/work_item_parent_with_edit.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -24,7 +25,8 @@ describe('WorkItemAttributesWrapper component', () => {
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
- const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
+ const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestoneWithEdit);
+ const findWorkItemMilestoneInline = () => wrapper.findComponent(WorkItemMilestoneInline);
const findWorkItemParentInline = () => wrapper.findComponent(WorkItemParentInline);
const findWorkItemParent = () => wrapper.findComponent(WorkItemParent);
@@ -110,6 +112,26 @@ describe('WorkItemAttributesWrapper component', () => {
expect(findWorkItemMilestone().exists()).toBe(exists);
});
+
+ it.each`
+ description | milestoneWidgetInlinePresent | milestoneWidgetWithEditPresent | workItemsMvc2FlagEnabled
+ ${'renders WorkItemMilestone when workItemsMvc2 enabled'} | ${false} | ${true} | ${true}
+ ${'renders WorkItemMilestoneInline when workItemsMvc2 disabled'} | ${true} | ${false} | ${false}
+ `(
+ '$description',
+ async ({
+ milestoneWidgetInlinePresent,
+ milestoneWidgetWithEditPresent,
+ workItemsMvc2FlagEnabled,
+ }) => {
+ createComponent({ workItemsMvc2: workItemsMvc2FlagEnabled });
+
+ await waitForPromises();
+
+ expect(findWorkItemMilestone().exists()).toBe(milestoneWidgetWithEditPresent);
+ expect(findWorkItemMilestoneInline().exists()).toBe(milestoneWidgetInlinePresent);
+ },
+ );
});
describe('parent widget', () => {
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_inline_spec.js
index fc2c5eb2af2..75c5763914a 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_inline_spec.js
@@ -2,7 +2,9 @@ import { GlCollapsibleListbox, GlListboxItem, GlSkeletonLoader, GlFormGroup } fr
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import WorkItemMilestone, { noMilestoneId } from '~/work_items/components/work_item_milestone.vue';
+import WorkItemMilestoneInline, {
+ noMilestoneId,
+} from '~/work_items/components/work_item_milestone_inline.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -18,7 +20,7 @@ import {
updateWorkItemMutationResponse,
} from '../mock_data';
-describe('WorkItemMilestone component', () => {
+describe('WorkItemMilestoneInline component', () => {
Vue.use(VueApollo);
let wrapper;
@@ -51,7 +53,7 @@ describe('WorkItemMilestone component', () => {
searchQueryHandler = successSearchQueryHandler,
mutationHandler = successUpdateWorkItemMutationHandler,
} = {}) => {
- wrapper = shallowMountExtended(WorkItemMilestone, {
+ wrapper = shallowMountExtended(WorkItemMilestoneInline, {
apolloProvider: createMockApollo([
[projectMilestonesQuery, searchQueryHandler],
[updateWorkItemMutation, mutationHandler],
@@ -73,13 +75,13 @@ describe('WorkItemMilestone component', () => {
createComponent();
expect(findInputGroup().exists()).toBe(true);
- expect(findInputGroup().attributes('label')).toBe(WorkItemMilestone.i18n.MILESTONE);
+ expect(findInputGroup().attributes('label')).toBe(WorkItemMilestoneInline.i18n.MILESTONE);
});
describe('Default text with canUpdate false and milestone value', () => {
describe.each`
description | milestone | value
- ${'when no milestone'} | ${null} | ${WorkItemMilestone.i18n.NONE}
+ ${'when no milestone'} | ${null} | ${WorkItemMilestoneInline.i18n.NONE}
${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title}
`('$description', ({ milestone, value }) => {
it(`has a value of "${value}"`, () => {
@@ -95,7 +97,9 @@ describe('WorkItemMilestone component', () => {
it(`has a value of "Add to milestone"`, () => {
createComponent({ canUpdate: true, milestone: null });
- expect(findDropdown().props('toggleText')).toBe(WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER);
+ expect(findDropdown().props('toggleText')).toBe(
+ WorkItemMilestoneInline.i18n.MILESTONE_PLACEHOLDER,
+ );
});
});
@@ -111,7 +115,7 @@ describe('WorkItemMilestone component', () => {
searchQueryHandler: successSearchWithNoMatchingMilestones,
});
- expect(findNoResultsText().text()).toBe(WorkItemMilestone.i18n.NO_MATCHING_RESULTS);
+ expect(findNoResultsText().text()).toBe(WorkItemMilestoneInline.i18n.NO_MATCHING_RESULTS);
expect(findDropdownItems()).toHaveLength(1);
});
});
@@ -162,7 +166,7 @@ describe('WorkItemMilestone component', () => {
await waitForPromises();
expect(findDropdown().props()).toMatchObject({
loading: false,
- toggleText: WorkItemMilestone.i18n.MILESTONE_PLACEHOLDER,
+ toggleText: WorkItemMilestoneInline.i18n.MILESTONE_PLACEHOLDER,
toggleClass: expect.arrayContaining(['gl-text-gray-500!']),
});
});
diff --git a/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js b/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js
new file mode 100644
index 00000000000..58a57978126
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js
@@ -0,0 +1,209 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone_with_edit.vue';
+import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import {
+ projectMilestonesResponse,
+ projectMilestonesResponseWithNoMilestones,
+ mockMilestoneWidgetResponse,
+ updateWorkItemMutationErrorResponse,
+ updateWorkItemMutationResponse,
+} from '../mock_data';
+
+describe('WorkItemMilestoneWithEdit component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const workItemType = 'Task';
+
+ const findSidebarDropdownWidget = () =>
+ wrapper.findComponent(WorkItemSidebarDropdownWidgetWithEdit);
+
+ const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
+ const successSearchWithNoMatchingMilestones = jest
+ .fn()
+ .mockResolvedValue(projectMilestonesResponseWithNoMilestones);
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
+ const showDropdown = () => findSidebarDropdownWidget().vm.$emit('dropdownShown');
+
+ const createComponent = ({
+ mountFn = shallowMountExtended,
+ canUpdate = true,
+ milestone = mockMilestoneWidgetResponse,
+ searchQueryHandler = successSearchQueryHandler,
+ mutationHandler = successUpdateWorkItemMutationHandler,
+ } = {}) => {
+ wrapper = mountFn(WorkItemMilestone, {
+ apolloProvider: createMockApollo([
+ [projectMilestonesQuery, searchQueryHandler],
+ [updateWorkItemMutation, mutationHandler],
+ ]),
+ propsData: {
+ fullPath: 'full-path',
+ canUpdate,
+ workItemMilestone: milestone,
+ workItemId,
+ workItemType,
+ },
+ });
+ };
+
+ it('has "Milestone" label', () => {
+ createComponent();
+
+ expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Milestone');
+ });
+
+ describe('Default text with canUpdate false and milestone value', () => {
+ describe.each`
+ description | milestone | value
+ ${'when no milestone'} | ${null} | ${'None'}
+ ${'when milestone set'} | ${mockMilestoneWidgetResponse} | ${mockMilestoneWidgetResponse.title}
+ `('$description', ({ milestone, value }) => {
+ it(`has a value of "${value}"`, () => {
+ createComponent({ mountFn: mountExtended, canUpdate: false, milestone });
+
+ expect(findSidebarDropdownWidget().props('canUpdate')).toBe(false);
+ expect(wrapper.text()).toContain(value);
+ });
+ });
+ });
+
+ describe('Dropdown search', () => {
+ it('shows no matching results when no items', () => {
+ createComponent({
+ searchQueryHandler: successSearchWithNoMatchingMilestones,
+ });
+
+ expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(0);
+ });
+ });
+
+ describe('Dropdown options', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: true });
+ });
+
+ it('calls successSearchQueryHandler with variables when dropdown is opened', async () => {
+ showDropdown();
+ await nextTick();
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ await waitForPromises();
+
+ expect(successSearchQueryHandler).toHaveBeenCalledWith({
+ first: 20,
+ fullPath: 'full-path',
+ state: 'active',
+ title: '',
+ });
+ });
+
+ it('shows the skeleton loader when the items are being fetched on click', async () => {
+ showDropdown();
+ await nextTick();
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ await nextTick();
+
+ expect(findSidebarDropdownWidget().props('loading')).toBe(true);
+ });
+
+ it('shows the milestones in dropdown when the items have finished fetching', async () => {
+ showDropdown();
+ await nextTick();
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ await waitForPromises();
+
+ expect(findSidebarDropdownWidget().props('loading')).toBe(false);
+ expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(
+ projectMilestonesResponse.data.workspace.attributes.nodes.length,
+ );
+ });
+
+ it('changes the milestone to null when clicked on no milestone', async () => {
+ showDropdown();
+ await nextTick();
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ findSidebarDropdownWidget().vm.$emit('updateValue', null);
+
+ await nextTick();
+ expect(findSidebarDropdownWidget().props('updateInProgress')).toBe(true);
+
+ await waitForPromises();
+ expect(findSidebarDropdownWidget().props('updateInProgress')).toBe(false);
+ expect(findSidebarDropdownWidget().props('itemValue')).toBe(null);
+ });
+
+ it('changes the milestone to the selected milestone', async () => {
+ const milestoneAtIndex = projectMilestonesResponse.data.workspace.attributes.nodes[0];
+
+ showDropdown();
+ await nextTick();
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ await waitForPromises();
+ findSidebarDropdownWidget().vm.$emit('updateValue', milestoneAtIndex.id);
+
+ await nextTick();
+
+ expect(findSidebarDropdownWidget().props('itemValue').title).toBe(milestoneAtIndex.title);
+ });
+ });
+
+ describe('Error handlers', () => {
+ it.each`
+ errorType | expectedErrorMessage | mockValue | resolveFunction
+ ${'graphql error'} | ${'Something went wrong while updating the task. Please try again.'} | ${updateWorkItemMutationErrorResponse} | ${'mockResolvedValue'}
+ ${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${new Error()} | ${'mockRejectedValue'}
+ `(
+ 'emits an error when there is a $errorType',
+ async ({ mockValue, expectedErrorMessage, resolveFunction }) => {
+ createComponent({
+ mutationHandler: jest.fn()[resolveFunction](mockValue),
+ canUpdate: true,
+ });
+
+ showDropdown();
+ findSidebarDropdownWidget().vm.$emit('updateValue', null);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[expectedErrorMessage]]);
+ },
+ );
+ });
+
+ describe('Tracking event', () => {
+ it('tracks updating the milestone', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ createComponent({ canUpdate: true });
+
+ showDropdown();
+ findSidebarDropdownWidget().vm.$emit('updateValue', null);
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_milestone', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: 'type_Task',
+ });
+ });
+ });
+});
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index a6c2b593e40..8ce69d12b3f 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -3,45 +3,58 @@
require 'spec_helper'
RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_projects do
- let!(:user) { create(:user) }
- let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+ let_it_be(:user, reload: true) { create(:user) }
+ let(:current_user) { user }
+ let(:group_params) { { path: 'group_path', visibility_level: Gitlab::VisibilityLevel::PUBLIC }.merge(extra_params) }
+ let(:extra_params) { {} }
+ let(:created_group) { response }
- subject(:execute) { service.execute }
+ subject(:response) { described_class.new(current_user, group_params).execute }
shared_examples 'has sync-ed traversal_ids' do
- specify { expect(subject.reload.traversal_ids).to eq([subject.parent&.traversal_ids, subject.id].flatten.compact) }
+ specify do
+ expect(created_group.traversal_ids).to eq([created_group.parent&.traversal_ids, created_group.id].flatten.compact)
+ end
+ end
+
+ shared_examples 'creating a group' do
+ specify do
+ expect { response }.to change { Group.count }
+ expect(created_group).to be_persisted
+ end
end
- describe 'visibility level restrictions' do
- let!(:service) { described_class.new(user, group_params) }
+ shared_examples 'does not create a group' do
+ specify do
+ expect { response }.not_to change { Group.count }
+ expect(created_group).not_to be_persisted
+ end
+ end
- context "create groups without restricted visibility level" do
- it { is_expected.to be_persisted }
+ context 'for visibility level restrictions' do
+ context 'without restricted visibility level' do
+ it_behaves_like 'creating a group'
end
- context "cannot create group with restricted visibility level" do
+ context 'with restricted visibility level' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC])
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- it { is_expected.not_to be_persisted }
+ it_behaves_like 'does not create a group'
end
end
- context 'when `setup_for_company:true` is passed' do
- let(:params) { group_params.merge(setup_for_company: true) }
- let(:service) { described_class.new(user, params) }
- let(:created_group) { service.execute }
+ context 'with `setup_for_company` attribute' do
+ let(:extra_params) { { setup_for_company: true } }
- it 'creates group with the specified setup_for_company' do
+ it 'has the specified setup_for_company' do
expect(created_group.setup_for_company).to eq(true)
end
end
- context 'creating a group with `default_branch_protection` attribute' do
- let(:params) { group_params.merge(default_branch_protection: Gitlab::Access::PROTECTION_NONE) }
- let(:service) { described_class.new(user, params) }
- let(:created_group) { service.execute }
+ context 'with `default_branch_protection` attribute' do
+ let(:extra_params) { { default_branch_protection: Gitlab::Access::PROTECTION_NONE } }
context 'for users who have the ability to create a group with `default_branch_protection`' do
it 'creates group with the specified branch protection level' do
@@ -52,23 +65,22 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
context 'for users who do not have the ability to create a group with `default_branch_protection`' do
it 'does not create the group with the specified branch protection level' do
allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection) { false }
+ allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection).and_return(false)
expect(created_group.default_branch_protection).not_to eq(Gitlab::Access::PROTECTION_NONE)
end
end
end
- context 'creating a group with `default_branch_protection_defaults` attribute' do
+ context 'with `default_branch_protection_defaults` attribute' do
let(:branch_protection) { ::Gitlab::Access::BranchProtection.protected_against_developer_pushes.stringify_keys }
- let(:params) { group_params.merge(default_branch_protection_defaults: branch_protection) }
- let(:service) { described_class.new(user, params) }
- let(:created_group) { service.execute }
+ let(:extra_params) { { default_branch_protection_defaults: branch_protection } }
context 'for users who have the ability to create a group with `default_branch_protection`' do
before do
allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :update_default_branch_protection, an_instance_of(Group)).and_return(true)
+ allow(Ability)
+ .to receive(:allowed?).with(user, :update_default_branch_protection, an_instance_of(Group)).and_return(true)
end
it 'creates group with the specified default branch protection settings' do
@@ -79,31 +91,26 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
context 'for users who do not have the ability to create a group with `default_branch_protection_defaults`' do
it 'does not create the group with the specified default branch protection settings' do
allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection) { false }
+ allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection).and_return(false)
expect(created_group.default_branch_protection_defaults).not_to eq(Gitlab::Access::PROTECTION_NONE)
end
end
end
- context 'creating a group with `allow_mfa_for_subgroups` attribute' do
- let(:params) { group_params.merge(allow_mfa_for_subgroups: false) }
- let(:service) { described_class.new(user, params) }
+ context 'with `allow_mfa_for_subgroups` attribute' do
+ let(:extra_params) { { allow_mfa_for_subgroups: false } }
- it 'creates group without error' do
- expect(service.execute).to be_persisted
- end
+ it_behaves_like 'creating a group'
end
- describe 'creating a top level group' do
- let(:service) { described_class.new(user, group_params) }
-
+ context 'for a top level group' do
context 'when user can create a group' do
before do
user.update_attribute(:can_create_group, true)
end
- it { is_expected.to be_persisted }
+ it_behaves_like 'creating a group'
context 'with before_commit callback' do
it_behaves_like 'has sync-ed traversal_ids'
@@ -115,187 +122,167 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
user.update_attribute(:can_create_group, false)
end
- it { is_expected.not_to be_persisted }
+ it_behaves_like 'does not create a group'
end
end
- describe 'creating a group within an organization' do
- let(:current_user) { user }
- let(:service) { described_class.new(current_user, params) }
-
+ context 'when creating a group within an organization' do
context 'when organization is provided' do
let_it_be(:organization) { create(:organization) }
- let(:params) { group_params.merge(organization_id: organization.id) }
+ let(:extra_params) { { organization_id: organization.id } }
context 'when user can create the group' do
before do
create(:organization_user, user: user, organization: organization)
end
- it { is_expected.to be_persisted }
+ it_behaves_like 'creating a group'
end
context 'when user is an admin', :enable_admin_mode do
let(:current_user) { create(:admin) }
- it { is_expected.to be_persisted }
+ it_behaves_like 'creating a group'
end
context 'when user can not create the group' do
- it 'does not save group and returns an error' do
- expect(execute).not_to be_persisted
- expect(execute.errors[:organization_id].first)
+ it_behaves_like 'does not create a group'
+
+ it 'returns an error and does not set organization_id' do
+ expect(created_group.errors[:organization_id].first)
.to eq(s_("CreateGroup|You don't have permission to create a group in the provided organization."))
- expect(execute.organization_id).to be_nil
+ expect(created_group.organization_id).to be_nil
end
end
end
context 'when organization is the default organization and not set by params' do
- let(:params) { group_params }
-
before do
create(:organization, :default)
end
- it { is_expected.to be_persisted }
+ it_behaves_like 'creating a group'
end
end
- describe 'creating subgroup' do
- let!(:group) { create(:group) }
- let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
+ context 'for a subgroup' do
+ let_it_be(:group) { create(:group) }
+ let(:extra_params) { { parent_id: group.id } }
context 'as group owner' do
- before do
+ before_all do
group.add_owner(user)
end
- it { is_expected.to be_persisted }
-
+ it_behaves_like 'creating a group'
it_behaves_like 'has sync-ed traversal_ids'
end
context 'as guest' do
- it 'does not save group and returns an error' do
- is_expected.not_to be_persisted
+ it_behaves_like 'does not create a group'
- expect(subject.errors[:parent_id].first).to eq(s_('CreateGroup|You don’t have permission to create a subgroup in this group.'))
- expect(subject.parent_id).to be_nil
+ it 'returns an error and does not set parent_id' do
+ expect(created_group.errors[:parent_id].first)
+ .to eq(s_('CreateGroup|You don’t have permission to create a subgroup in this group.'))
+ expect(created_group.parent_id).to be_nil
end
end
context 'as owner' do
- before do
+ before_all do
group.add_owner(user)
end
- it { is_expected.to be_persisted }
+ it_behaves_like 'creating a group'
end
context 'as maintainer' do
- before do
+ before_all do
group.add_maintainer(user)
end
- it { is_expected.to be_persisted }
+ it_behaves_like 'creating a group'
end
end
- describe "when visibility level is passed as a string" do
- let(:service) { described_class.new(user, group_params) }
- let(:group_params) { { path: 'group_path', visibility: 'public' } }
-
- it "assigns the correct visibility level" do
- group = service.execute
+ context 'when visibility level is passed as a string' do
+ let(:extra_params) { { visibility: 'public' } }
- expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ it 'assigns the correct visibility level' do
+ expect(created_group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
end
- describe 'creating a mattermost team' do
- let!(:params) { group_params.merge(create_chat_team: "true") }
- let!(:service) { described_class.new(user, params) }
+ context 'for creating a mattermost team' do
+ let(:extra_params) { { create_chat_team: 'true' } }
before do
stub_mattermost_setting(enabled: true)
end
it 'create the chat team with the group' do
- allow_any_instance_of(::Mattermost::Team).to receive(:create)
- .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
+ allow_next_instance_of(::Mattermost::Team) do |instance|
+ allow(instance).to receive(:create).and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
+ end
- expect { subject }.to change { ChatTeam.count }.from(0).to(1)
+ expect { response }.to change { ChatTeam.count }.from(0).to(1)
end
end
- describe 'creating a setting record' do
- let(:service) { described_class.new(user, group_params) }
-
+ context 'for creating a setting record' do
it 'create the settings record connected to the group' do
- group = subject
- expect(group.namespace_settings).to be_persisted
+ expect(created_group.namespace_settings).to be_persisted
end
end
- describe 'creating a details record' do
- let(:service) { described_class.new(user, group_params) }
-
+ context 'for creating a details record' do
it 'create the details record connected to the group' do
- group = subject
- expect(group.namespace_details).to be_persisted
+ expect(created_group.namespace_details).to be_persisted
end
end
- describe 'create service for the group' do
- let(:service) { described_class.new(user, group_params) }
- let(:created_group) { service.execute }
+ context 'with an active instance-level integration' do
+ let_it_be(:instance_integration) do
+ create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/')
+ end
+
+ it 'creates a service from the instance-level integration' do
+ expect(created_group.integrations.count).to eq(1)
+ expect(created_group.integrations.first.api_url).to eq(instance_integration.api_url)
+ expect(created_group.integrations.first.inherit_from_id).to eq(instance_integration.id)
+ end
- context 'with an active instance-level integration' do
- let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') }
+ context 'with an active group-level integration' do
+ let(:extra_params) { { parent_id: group.id } }
+ let_it_be(:group) { create(:group) { |g| g.add_owner(user) } }
+ let_it_be(:group_integration) do
+ create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/')
+ end
- it 'creates a service from the instance-level integration' do
+ it 'creates a service from the group-level integration' do
expect(created_group.integrations.count).to eq(1)
- expect(created_group.integrations.first.api_url).to eq(instance_integration.api_url)
- expect(created_group.integrations.first.inherit_from_id).to eq(instance_integration.id)
+ expect(created_group.integrations.first.api_url).to eq(group_integration.api_url)
+ expect(created_group.integrations.first.inherit_from_id).to eq(group_integration.id)
end
- context 'with an active group-level integration' do
- let(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
- let!(:group_integration) { create(:prometheus_integration, :group, group: group, api_url: 'https://prometheus.group.com/') }
- let(:group) do
- create(:group).tap do |group|
- group.add_owner(user)
- end
+ context 'with an active subgroup' do
+ let(:extra_params) { { parent_id: subgroup.id } }
+ let_it_be(:subgroup) { create(:group, parent: group) { |g| g.add_owner(user) } }
+ let_it_be(:subgroup_integration) do
+ create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/')
end
- it 'creates a service from the group-level integration' do
+ it 'creates a service from the subgroup-level integration' do
expect(created_group.integrations.count).to eq(1)
- expect(created_group.integrations.first.api_url).to eq(group_integration.api_url)
- expect(created_group.integrations.first.inherit_from_id).to eq(group_integration.id)
- end
-
- context 'with an active subgroup' do
- let(:service) { described_class.new(user, group_params.merge(parent_id: subgroup.id)) }
- let!(:subgroup_integration) { create(:prometheus_integration, :group, group: subgroup, api_url: 'https://prometheus.subgroup.com/') }
- let(:subgroup) do
- create(:group, parent: group).tap do |subgroup|
- subgroup.add_owner(user)
- end
- end
-
- it 'creates a service from the subgroup-level integration' do
- expect(created_group.integrations.count).to eq(1)
- expect(created_group.integrations.first.api_url).to eq(subgroup_integration.api_url)
- expect(created_group.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
- end
+ expect(created_group.integrations.first.api_url).to eq(subgroup_integration.api_url)
+ expect(created_group.integrations.first.inherit_from_id).to eq(subgroup_integration.id)
end
end
end
end
- context 'shared runners configuration' do
- context 'parent group present' do
+ context 'with shared runners configuration' do
+ context 'when parent group is present' do
using RSpec::Parameterized::TableSyntax
where(:shared_runners_config, :descendants_override_disabled_shared_runners_config) do
@@ -306,30 +293,31 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
end
with_them do
- let!(:group) { create(:group, shared_runners_enabled: shared_runners_config, allow_descendants_override_disabled_shared_runners: descendants_override_disabled_shared_runners_config) }
- let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
+ let(:extra_params) { { parent_id: group.id } }
+ let(:group) do
+ create(
+ :group,
+ shared_runners_enabled: shared_runners_config,
+ allow_descendants_override_disabled_shared_runners: descendants_override_disabled_shared_runners_config
+ )
+ end
before do
group.add_owner(user)
end
it 'creates group following the parent config' do
- new_group = service.execute
-
- expect(new_group.shared_runners_enabled).to eq(shared_runners_config)
- expect(new_group.allow_descendants_override_disabled_shared_runners).to eq(descendants_override_disabled_shared_runners_config)
+ expect(created_group.shared_runners_enabled).to eq(shared_runners_config)
+ expect(created_group.allow_descendants_override_disabled_shared_runners)
+ .to eq(descendants_override_disabled_shared_runners_config)
end
end
end
- context 'root group' do
- let!(:service) { described_class.new(user) }
-
+ context 'for root group' do
it 'follows default config' do
- new_group = service.execute
-
- expect(new_group.shared_runners_enabled).to eq(true)
- expect(new_group.allow_descendants_override_disabled_shared_runners).to eq(false)
+ expect(created_group.shared_runners_enabled).to eq(true)
+ expect(created_group.allow_descendants_override_disabled_shared_runners).to eq(false)
end
end
end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 3f04d984e55..d54dcc8a31d 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -14,12 +14,14 @@ module CycleAnalyticsHelpers
page.all('.gl-path-button').collect(&:text).map { |name_with_median| name_with_median.split("\n")[0] }
end
- def fill_in_custom_stage_fields
+ def fill_in_custom_stage_fields(stage_name = nil)
index = page.all('[data-testid="value-stream-stage-fields"]').length
last_stage = page.all('[data-testid="value-stream-stage-fields"]').last
+ stage_name = "Cool custom stage - name #{index}" if stage_name.blank?
+
within last_stage do
- find('[name*="custom-stage-name-"]').fill_in with: "Cool custom stage - name #{index}"
+ find('[name*="custom-stage-name-"]').fill_in with: stage_name
select_dropdown_option_by_value "custom-stage-start-event-", 'Merge request created'
select_dropdown_option_by_value "custom-stage-end-event-", 'Merge request merged'
end
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 7ff37030493..0f35681ca7d 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -1,16 +1,20 @@
# frozen_string_literal: true
+RSpec.shared_context 'with work_items_mvc_2' do |flag|
+ before do
+ stub_feature_flags(work_items_mvc_2: flag)
+
+ page.refresh
+ wait_for_all_requests
+ end
+end
+
RSpec.shared_examples 'work items title' do
let(:title_selector) { '[data-testid="work-item-title"]' }
let(:title_with_edit_selector) { '[data-testid="work-item-title-with-edit"]' }
context 'when the work_items_mvc_2 FF is disabled' do
- before do
- stub_feature_flags(work_items_mvc_2: false)
-
- page.refresh
- wait_for_all_requests
- end
+ include_context 'with work_items_mvc_2', false
it 'successfully shows and changes the title of the work item' do
expect(work_item.reload.title).to eq work_item.title
@@ -24,17 +28,12 @@ RSpec.shared_examples 'work items title' do
end
context 'when the work_items_mvc_2 FF is enabled' do
- before do
- stub_feature_flags(work_items_mvc_2: true)
-
- page.refresh
- wait_for_all_requests
- end
+ include_context 'with work_items_mvc_2', true
it 'successfully shows and changes the title of the work item' do
expect(work_item.reload.title).to eq work_item.title
- click_button 'Edit'
+ click_button 'Edit', match: :first
find(title_with_edit_selector).set("Work item title")
send_keys([:command, :enter])
wait_for_requests
@@ -333,15 +332,10 @@ RSpec.shared_examples 'work items description' do
[true, false].each do |work_items_mvc_2_flag| # rubocop:disable RSpec/UselessDynamicDefinition -- check it for both off and on
let(:edit_button) { work_items_mvc_2_flag ? 'Edit' : 'Edit description' }
- before do
- stub_feature_flags(work_items_mvc_2: work_items_mvc_2_flag)
-
- page.refresh
- wait_for_all_requests
- end
+ include_context 'with work_items_mvc_2', work_items_mvc_2_flag
it 'shows GFM autocomplete', :aggregate_failures do
- click_button edit_button
+ click_button edit_button, match: :first
fill_in _('Description'), with: "@#{user.username}"
page.within('.atwho-container') do
@@ -350,7 +344,7 @@ RSpec.shared_examples 'work items description' do
end
it 'autocompletes available quick actions', :aggregate_failures do
- click_button edit_button
+ click_button edit_button, match: :first
fill_in _('Description'), with: '/'
page.within('#at-view-commands') do
@@ -371,7 +365,7 @@ RSpec.shared_examples 'work items description' do
end
it 'shows conflict message when description changes', :aggregate_failures do
- click_button edit_button
+ click_button edit_button, match: :first
::WorkItems::UpdateService.new(
container: work_item.project,
@@ -411,17 +405,61 @@ RSpec.shared_examples 'work items invite members' do
end
RSpec.shared_examples 'work items milestone' do
- it 'searches and sets or removes milestone for the work item' do
- click_button s_('WorkItem|Add to milestone')
- send_keys "\"#{milestone.title}\""
- select_listbox_item(milestone.title, exact_text: true)
+ context 'on work_items_mvc_2 FF off' do
+ include_context 'with work_items_mvc_2', false
+
+ it 'searches and sets or removes milestone for the work item' do
+ click_button s_('WorkItem|Add to milestone')
+ send_keys "\"#{milestone.title}\""
+ select_listbox_item(milestone.title, exact_text: true)
+
+ expect(page).to have_button(milestone.title)
+
+ click_button milestone.title
+ select_listbox_item(s_('WorkItem|No milestone'), exact_text: true)
+
+ expect(page).to have_button(s_('WorkItem|Add to milestone'))
+ end
+ end
- expect(page).to have_button(milestone.title)
+ context 'on work_items_mvc_2 FF on' do
+ let(:work_item_milestone_selector) { '[data-testid="work-item-milestone-with-edit"]' }
- click_button milestone.title
- select_listbox_item(s_('WorkItem|No milestone'), exact_text: true)
+ include_context 'with work_items_mvc_2', true
- expect(page).to have_button(s_('WorkItem|Add to milestone'))
+ it 'passes axe automated accessibility testing in closed state' do
+ expect(page).to be_axe_clean.within(work_item_milestone_selector)
+ end
+
+ context 'when edit is clicked' do
+ it 'selects and updates the right milestone', :aggregate_failures do
+ find_and_click_edit(work_item_milestone_selector)
+
+ select_listbox_item(milestones[10].title)
+
+ wait_for_requests
+ within(work_item_milestone_selector) do
+ expect(page).to have_text(milestones[10].title)
+ end
+
+ find_and_click_edit(work_item_milestone_selector)
+
+ find_and_click_clear(work_item_milestone_selector)
+
+ expect(find(work_item_milestone_selector)).to have_content('None')
+ end
+
+ it 'searches and sets or removes milestone for the work item' do
+ find_and_click_edit(work_item_milestone_selector)
+ within(work_item_milestone_selector) do
+ send_keys "\"#{milestones[11].title}\""
+ wait_for_requests
+
+ select_listbox_item(milestones[11].title)
+ expect(page).to have_text(milestones[11].title)
+ end
+ end
+ end
end
end
@@ -603,12 +641,7 @@ RSpec.shared_examples 'work items iteration' do
)
end
- before do
- stub_feature_flags(work_items_mvc_2: true)
-
- page.refresh
- wait_for_all_requests
- end
+ include_context 'with work_items_mvc_2', true
context 'for accessibility' do
it 'has the work item iteration with edit' do
diff --git a/workhorse/go.mod b/workhorse/go.mod
index 099e3723193..30a228219a5 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -6,7 +6,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.1
github.com/BurntSushi/toml v1.3.2
github.com/alecthomas/chroma/v2 v2.12.0
- github.com/aws/aws-sdk-go v1.48.11
+ github.com/aws/aws-sdk-go v1.49.3
github.com/disintegration/imaging v1.6.2
github.com/getsentry/raven-go v0.2.0
github.com/golang-jwt/jwt/v5 v5.2.0
diff --git a/workhorse/go.sum b/workhorse/go.sum
index c203d78a738..33bfccceb71 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -94,8 +94,8 @@ github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurG
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
-github.com/aws/aws-sdk-go v1.48.11 h1:9YbiSbaF/jWi+qLRl+J5dEhr2mcbDYHmKg2V7RBcD5M=
-github.com/aws/aws-sdk-go v1.48.11/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go v1.49.3 h1:+UGwhC3kChk0pRCxSsbaQSNIc8MfFURQL44Ig6RRR3I=
+github.com/aws/aws-sdk-go v1.49.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc=
github.com/aws/aws-sdk-go-v2/config v1.25.5 h1:UGKm9hpQS2hoK8CEJ1BzAW8NbUpvwDJJ4lyqXSzu8bk=