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_manual_todo.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/constants.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js8
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_modal.vue67
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_trigger.vue43
-rw-r--r--app/assets/javascripts/invite_member/constants.js2
-rw-r--r--app/assets/javascripts/invite_member/event_hub.js3
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_modal.js27
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_trigger.js18
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue44
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js4
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/helpers/invite_members_helper.rb14
-rw-r--r--app/helpers/issuables_helper.rb3
-rw-r--r--app/helpers/sidebars_helper.rb3
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/concerns/atomic_internal_id.rb4
-rw-r--r--app/models/member.rb3
-rw-r--r--app/models/project.rb2
-rw-r--r--app/views/layouts/nav/sidebar/_project_menus.html.haml46
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml23
-rw-r--r--changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml5
-rw-r--r--changelogs/unreleased/327405-retry-failed-background-migrations.yml5
-rw-r--r--config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml8
-rw-r--r--db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb17
-rw-r--r--db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb2
-rw-r--r--db/schema_migrations/202104270628071
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/pages/index.md29
-rw-r--r--doc/ci/environments/index.md2
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/development/migration_style_guide.md2
-rw-r--r--doc/user/project/pages/introduction.md4
-rw-r--r--lib/gitlab/database/background_migration/batched_job.rb13
-rw-r--r--lib/gitlab/database/background_migration/batched_migration.rb3
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_runner.rb22
-rw-r--r--lib/gitlab/database/background_migration/batched_migration_wrapper.rb2
-rw-r--r--lib/gitlab/experimentation.rb4
-rw-r--r--lib/gitlab/object_hierarchy.rb34
-rw-r--r--lib/sidebars/projects/menus/ci_cd_menu.rb114
-rw-r--r--lib/sidebars/projects/panel.rb1
-rw-r--r--locale/gitlab.pot38
-rw-r--r--qa/qa/page/project/sub_menus/ci_cd.rb6
-rw-r--r--spec/deprecation_toolkit_env.rb1
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb25
-rw-r--r--spec/frontend/boards/stores/actions_spec.js9
-rw-r--r--spec/frontend/invite_member/components/invite_member_modal_spec.js67
-rw-r--r--spec/frontend/invite_member/components/invite_member_trigger_mock_data.js7
-rw-r--r--spec/frontend/invite_member/components/invite_member_trigger_spec.js48
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js12
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js38
-rw-r--r--spec/frontend/users_select/test_helper.js8
-rw-r--r--spec/helpers/invite_members_helper_spec.rb47
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb36
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb101
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb36
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb1
-rw-r--r--spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb66
-rw-r--r--spec/models/concerns/integration_spec.rb (renamed from spec/models/integration_spec.rb)0
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb27
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb100
68 files changed, 590 insertions, 709 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index deb3d8f3df4..a50a96e7cb8 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -385,8 +385,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/controllers/subscriptions_controller_spec.rb
- ee/spec/features/ci_shared_runner_warnings_spec.rb
- ee/spec/features/integrations/jira/jira_issues_list_spec.rb
- - ee/spec/features/issues/bulk_assignment_epic_spec.rb
- - ee/spec/features/issues/user_uses_quick_actions_spec.rb
- ee/spec/features/markdown/metrics_spec.rb
- ee/spec/features/registrations/group_invites_during_signup_flow_spec.rb
- ee/spec/features/subscriptions_spec.rb
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index eba0eb0eea3..a1f074cd973 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-76be441c65a48b106fdfc06aa35c0d15bdf13b9b
+f6f340eff91d01a1e36e8c9c368d93c9bff5e4f5
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index aa574222fef..f94697172ac 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -84,7 +84,7 @@ export default {
return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
},
showListHeaderButton() {
- return !this.disabled && this.listType !== ListType.closed && !this.isEpicBoard;
+ return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return this.listType === ListType.milestone && this.list.milestone && this.showListDetails;
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 997655c346a..3d7f1f38a34 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -29,17 +29,17 @@ export default {
};
},
computed: {
- ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
+ ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
- return this.glFeatures.wipLimits;
+ return this.glFeatures.wipLimits && !this.isEpicBoard;
},
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.boardLists[this.activeId];
}
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
@@ -71,7 +71,7 @@ export default {
deleteBoard() {
// eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to remove this list?'))) {
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
this.removeList(this.activeId);
} else {
this.activeList.destroy();
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 76edb69c204..4519992ca94 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -2,6 +2,7 @@ import { __ } from '~/locale';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
+import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from './graphql/board_list_update.mutation.graphql';
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
@@ -73,6 +74,12 @@ export const updateListQueries = {
},
};
+export const deleteListQueries = {
+ [issuableTypes.issue]: {
+ mutation: destroyBoardListMutation,
+ },
+};
+
export const titleQueries = {
[issuableTypes.issue]: {
mutation: issueSetTitleMutation,
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 5f8db077586..de634e844dc 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -8,6 +8,7 @@ import {
titleQueries,
subscriptionQueries,
SupportedFilters,
+ deleteListQueries,
updateListQueries,
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
@@ -31,7 +32,6 @@ import {
getSupportedParams,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
-import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
@@ -265,14 +265,14 @@ export default {
commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
},
- removeList: ({ state, commit }, listId) => {
- const listsBackup = { ...state.boardLists };
+ removeList: ({ state: { issuableType, boardLists }, commit }, listId) => {
+ const listsBackup = { ...boardLists };
commit(types.REMOVE_LIST, listId);
return gqlClient
.mutate({
- mutation: destroyBoardListMutation,
+ mutation: deleteListQueries[issuableType].mutation,
variables: {
listId,
},
diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
deleted file mode 100644
index ec77e49ae53..00000000000
--- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<script>
-import { GlModal, GlLink } from '@gitlab/ui';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import { s__, __ } from '~/locale';
-import { OPEN_MODAL, MODAL_ID } from '../constants';
-import eventHub from '../event_hub';
-
-export default {
- cancelProps: {
- text: __('Got it'),
- attributes: [
- {
- variant: 'info',
- },
- ],
- },
- modalId: MODAL_ID,
- components: {
- GlLink,
- GlModal,
- },
- props: {
- membersPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- i18n: {
- modalTitle: s__("InviteMember|Oops, this feature isn't ready yet"),
- bodyTopMessage: s__(
- "InviteMember|We're working to allow everyone to invite new members, making it easier for teams to get started with GitLab",
- ),
- bodyMiddleMessage: s__(
- 'InviteMember|Until then, ask an owner to invite new project members for you',
- ),
- linkText: s__('InviteMember|See who can invite members for you'),
- },
- mounted() {
- eventHub.$on(OPEN_MODAL, this.openModal);
- },
- methods: {
- openModal() {
- this.$root.$emit(BV_SHOW_MODAL, MODAL_ID);
- },
- },
-};
-</script>
-<template>
- <gl-modal :modal-id="$options.modalId" size="sm" :action-cancel="$options.cancelProps">
- <template #modal-title>
- {{ $options.i18n.modalTitle }}
- <gl-emoji
- class="gl-vertical-align-baseline font-size-inherit gl-mr-1"
- data-name="sweat_smile"
- />
- </template>
- <p>{{ $options.i18n.bodyTopMessage }}</p>
- <p>{{ $options.i18n.bodyMiddleMessage }}</p>
- <gl-link
- :href="membersPath"
- data-track-event="click_who_can_invite_link"
- data-track-label="invite_members_message"
- >{{ $options.i18n.linkText }}</gl-link
- >
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
deleted file mode 100644
index ee89e0bbf71..00000000000
--- a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlLink } from '@gitlab/ui';
-import { OPEN_MODAL } from '../constants';
-import eventHub from '../event_hub';
-
-export default {
- components: {
- GlLink,
- },
- props: {
- displayText: {
- type: String,
- required: false,
- default: '',
- },
- event: {
- type: String,
- required: false,
- default: '',
- },
- label: {
- type: String,
- required: false,
- default: '',
- },
- },
- methods: {
- openModal() {
- eventHub.$emit(OPEN_MODAL);
- },
- },
-};
-</script>
-
-<template>
- <gl-link
- data-is-link="true"
- :data-track-event="event"
- :data-track-label="label"
- @click="openModal"
- >{{ displayText }}
- </gl-link>
-</template>
diff --git a/app/assets/javascripts/invite_member/constants.js b/app/assets/javascripts/invite_member/constants.js
deleted file mode 100644
index fee6e7a260a..00000000000
--- a/app/assets/javascripts/invite_member/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const OPEN_MODAL = 'openModal';
-export const MODAL_ID = 'invite-member-modal';
diff --git a/app/assets/javascripts/invite_member/event_hub.js b/app/assets/javascripts/invite_member/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/invite_member/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js
deleted file mode 100644
index a50d31c9e7a..00000000000
--- a/app/assets/javascripts/invite_member/init_invite_member_modal.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { GlToast } from '@gitlab/ui';
-import Vue from 'vue';
-import { isInIssuePage, isInDesignPage } from '~/lib/utils/common_utils';
-import InviteMemberModal from './components/invite_member_modal.vue';
-
-Vue.use(GlToast);
-
-const isAssigneesWidgetShown =
- (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget;
-
-export default function initInviteMembersModal() {
- const el = document.querySelector('.js-invite-member-modal');
-
- if (!el || isAssigneesWidgetShown) {
- return false;
- }
-
- const { membersPath } = el.dataset;
-
- return new Vue({
- el,
- render: (createElement) =>
- createElement(InviteMemberModal, {
- props: { membersPath },
- }),
- });
-}
diff --git a/app/assets/javascripts/invite_member/init_invite_member_trigger.js b/app/assets/javascripts/invite_member/init_invite_member_trigger.js
deleted file mode 100644
index eb765ae83b0..00000000000
--- a/app/assets/javascripts/invite_member/init_invite_member_trigger.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import InviteMemberTrigger from './components/invite_member_trigger.vue';
-
-export default function initInviteMembersTrigger() {
- const el = document.querySelector('.js-invite-member-trigger');
-
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- render: (createElement) =>
- createElement(InviteMemberTrigger, {
- props: { ...el.dataset },
- }),
- });
-}
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 2b679a83eac..3143ff5adac 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,8 +1,6 @@
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issuable_show/constants';
@@ -58,7 +56,5 @@ export default function initShowIssue() {
} else {
loadAwardsHandler();
}
- initInviteMemberModal();
- initInviteMemberTrigger();
}
}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index a5118e3529a..021122d7637 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -3,8 +3,6 @@ import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { handleLocationHash } from '~/lib/utils/common_utils';
@@ -28,8 +26,6 @@ export default function initMergeRequestShow() {
} else {
loadAwardsHandler();
}
- initInviteMemberModal();
- initInviteMemberTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 2fc25151d1c..cced8462955 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -47,9 +47,6 @@ export default {
directlyInviteMembers: {
default: false,
},
- indirectlyInviteMembers: {
- default: false,
- },
},
props: {
iid: {
@@ -444,7 +441,7 @@ export default {
</template>
<template #footer>
<gl-dropdown-item>
- <sidebar-invite-members v-if="directlyInviteMembers || indirectlyInviteMembers" />
+ <sidebar-invite-members v-if="directlyInviteMembers" />
</gl-dropdown-item>
</template>
</multi-select-dropdown>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
index 9952c6db582..5c32d03e0d4 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
@@ -1,51 +1,23 @@
<script>
-import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
-import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { __ } from '~/locale';
export default {
displayText: __('Invite members'),
dataTrackLabel: 'edit_assignee',
+ dataTrackEvent: 'click_invite_members',
components: {
- InviteMemberTrigger,
- InviteMemberModal,
InviteMembersTrigger,
},
- inject: {
- projectMembersPath: {
- default: '',
- },
- directlyInviteMembers: {
- default: false,
- },
- },
- computed: {
- trackEvent() {
- return this.directlyInviteMembers ? 'click_invite_members' : 'click_invite_members_version_b';
- },
- },
};
</script>
<template>
- <div>
- <invite-members-trigger
- v-if="directlyInviteMembers"
- trigger-element="anchor"
- :display-text="$options.displayText"
- :event="trackEvent"
- :label="$options.dataTrackLabel"
- classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
- />
- <template v-else>
- <invite-member-trigger
- :display-text="$options.displayText"
- :event="trackEvent"
- :label="$options.dataTrackLabel"
- class="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
- />
- <invite-member-modal :members-path="projectMembersPath" />
- </template>
- </div>
+ <invite-members-trigger
+ trigger-element="anchor"
+ :display-text="$options.displayText"
+ :event="$options.dataTrackEvent"
+ :label="$options.dataTrackLabel"
+ classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
+ />
</template>
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9115c3562d3..b1d35a1e6f4 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -86,7 +86,7 @@ function mountAssigneesComponent() {
if (!el) return;
- const { id, iid, fullPath, editable, projectMembersPath } = getSidebarOptions();
+ const { id, iid, fullPath, editable } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
@@ -96,9 +96,7 @@ function mountAssigneesComponent() {
},
provide: {
canUpdate: editable,
- projectMembersPath,
directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
- indirectlyInviteMembers: el.hasAttribute('data-indirectly-invite-members'),
},
render: (createElement) =>
createElement('sidebar-assignees-widget', {
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cae5cc411bc..dd9b9071fd8 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -56,8 +56,6 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
- record_experiment_user(:invite_members_version_b)
-
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a31437288b9..bf534c5fb04 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -47,8 +47,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
- record_experiment_user(:invite_members_version_b)
-
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 5cf47f4a990..2cad6f4745c 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -17,20 +17,6 @@ module InviteMembersHelper
end
end
- def indirectly_invite_members?
- strong_memoize(:indirectly_invite_members) do
- experiment_enabled?(:invite_members_version_b) && !can_import_members?
- end
- end
-
- def show_invite_members_track_event
- if directly_invite_members?
- 'show_invite_members'
- elsif indirectly_invite_members?
- 'show_invite_members_version_b'
- end
- end
-
def invite_group_members?(group)
experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index e5ea2920eaa..d149389b31f 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -390,8 +390,7 @@ module IssuablesHelper
severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
createNoteEmail: issuable[:create_note_email],
- issuableType: issuable[:type],
- projectMembersPath: project_project_members_path(@project, sort: :access_level_desc)
+ issuableType: issuable[:type]
}
end
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 0aa3aa20306..143919a68ab 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -40,7 +40,8 @@ module SidebarsHelper
container: project,
learn_gitlab_experiment_enabled: learn_gitlab_experiment_enabled?(project),
current_ref: current_ref,
- jira_issues_integration: project_jira_issues_integration?
+ jira_issues_integration: project_jira_issues_integration?,
+ can_view_pipeline_editor: can_view_pipeline_editor?(project)
}
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c630dff6fa8..c2dc9c5d859 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -908,7 +908,7 @@ module Ci
def same_family_pipeline_ids
::Gitlab::Ci::PipelineObjectHierarchy.new(
- self.class.where(id: root_ancestor), options: { same_project: true }
+ self.class.default_scoped.where(id: root_ancestor), options: { same_project: true }
).base_and_descendants.select(:id)
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index bbf9ecbcfe9..80cf6260b0b 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -214,9 +214,9 @@ module AtomicInternalId
def self.project_init(klass, column_name = :iid)
->(instance, scope) do
if instance
- klass.where(project_id: instance.project_id).maximum(column_name)
+ klass.default_scoped.where(project_id: instance.project_id).maximum(column_name)
elsif scope.present?
- klass.where(**scope).maximum(column_name)
+ klass.default_scoped.where(**scope).maximum(column_name)
end
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index e978552592d..ee9b2c8cef3 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -140,7 +140,8 @@ class Member < ApplicationRecord
scope :distinct_on_user_with_max_access_level, -> do
distinct_members = select('DISTINCT ON (user_id, invite_email) *')
.order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
- Member.from(distinct_members, :members)
+
+ from(distinct_members, :members)
end
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 2cddcc43c4b..b8b52c594c4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2528,7 +2528,7 @@ class Project < ApplicationRecord
namespace.root_ancestor.all_projects
.joins(:packages)
.where.not(id: id)
- .merge(Packages::Package.with_name(package_name))
+ .merge(Packages::Package.default_scoped.with_name(package_name))
.exists?
end
diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml
index 631fc3f095d..4826aca9f0a 100644
--- a/app/views/layouts/nav/sidebar/_project_menus.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml
@@ -1,49 +1,3 @@
-- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
- = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
- .nav-icon-container
- = sprite_icon('rocket')
- %span.nav-item-name#js-onboarding-pipelines-link
- = _('CI/CD')
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
- = link_to project_pipelines_path(@project) do
- %strong.fly-out-top-item-name
- = _('CI/CD')
- %li.divider.fly-out-top-item
- - if project_nav_tab? :pipelines
- = nav_link(path: ['pipelines#index', 'pipelines#show']) do
- = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
- %span
- = _('Pipelines')
-
- - if can_view_pipeline_editor?(@project)
- = nav_link(controller: :pipeline_editor, action: :show) do
- = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
- %span
- = s_('Pipelines|Editor')
-
- - if project_nav_tab? :builds
- = nav_link(controller: :jobs) do
- = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
- %span
- = _('Jobs')
-
- - if Feature.enabled?(:artifacts_management_page, @project)
- = nav_link(controller: :artifacts, action: :index) do
- = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
- %span
- = _('Artifacts')
-
- - if project_nav_tab?(:pipelines)
- = nav_link(controller: :pipeline_schedules) do
- = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
- %span
- = _('Schedules')
-
- = render_if_exists "layouts/nav/test_cases_link", project: @project
-
- if project_nav_tab? :security_and_compliance
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 47e7ff0e4bc..86369b32e98 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,7 +1,10 @@
- issuable_type = issuable_sidebar[:type]
- dropdown_options = assignees_dropdown_options(issuable_type)
-#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in, max_assignees: dropdown_options[:data][:"max-select"], directly_invite_members: directly_invite_members?, indirectly_invite_members: indirectly_invite_members? } }
+#js-vue-sidebar-assignees{ data: { field: issuable_type,
+ signed_in: signed_in,
+ max_assignees: dropdown_options[:data][:"max-select"],
+ directly_invite_members: directly_invite_members? } }
.title.hide-collapsed
= _('Assignee')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
@@ -39,12 +42,12 @@
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- - if directly_invite_members? || indirectly_invite_members?
+ - if directly_invite_members?
- options[:dropdown_class] += ' dropdown-extended-height'
- options[:footer_content] = true
- options[:wrapper_class] = 'js-sidebar-assignee-dropdown'
- options[:toggle_class] += ' js-invite-members-track'
- - data['track-event'] = show_invite_members_track_event
+ - data['track-event'] = 'show_invite_members'
- options[:data].merge!(data)
- invite_text = _('Invite Members')
- track_label = 'edit_assignee'
@@ -52,15 +55,9 @@
= dropdown_tag(title, options: options) do
%ul.dropdown-footer-list
%li
- - if directly_invite_members?
- .js-invite-members-trigger{ data: { trigger_element: 'anchor',
- display_text: invite_text,
- event: 'click_invite_members',
- label: track_label } }
- - else
- .js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } }
+ .js-invite-members-trigger{ data: { trigger_element: 'anchor',
+ display_text: invite_text,
+ event: 'click_invite_members',
+ label: track_label } }
- else
= dropdown_tag(title, options: options)
-
-- if indirectly_invite_members?
- .js-invite-member-modal{ data: { members_path: project_project_members_path(@project, sort: :access_level_desc) } }
diff --git a/changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml b/changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml
new file mode 100644
index 00000000000..3b484497362
--- /dev/null
+++ b/changelogs/unreleased/326251-experiment-cleanup-invite_members_version_b-in-assignee-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Remove invite_members_version_b experiment
+merge_request: 60426
+author:
+type: other
diff --git a/changelogs/unreleased/327405-retry-failed-background-migrations.yml b/changelogs/unreleased/327405-retry-failed-background-migrations.yml
new file mode 100644
index 00000000000..34c3e59850d
--- /dev/null
+++ b/changelogs/unreleased/327405-retry-failed-background-migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to batched migration jobs status
+merge_request: 60248
+author:
+type: other
diff --git a/config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml b/config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml
deleted file mode 100644
index 069e740ba44..00000000000
--- a/config/feature_flags/experiment/invite_members_version_b_experiment_percentage.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: invite_members_version_b_experiment_percentage
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43900
-rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/214
-milestone: '13.5'
-type: experiment
-group: group::expansion
-default_enabled: false
diff --git a/db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb b/db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb
new file mode 100644
index 00000000000..c429094762e
--- /dev/null
+++ b/db/migrate/20210427062807_add_index_to_batched_migration_jobs_status.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToBatchedMigrationJobsStatus < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_batched_jobs_on_batched_migration_id_and_status'
+
+ def up
+ add_concurrent_index :batched_background_migration_jobs, [:batched_background_migration_id, :status], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :batched_background_migration_jobs, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb b/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb
index 5fcec83bfc3..3ec6c59f166 100644
--- a/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb
+++ b/db/post_migrate/20191030223057_backfill_version_author_and_created_at.rb
@@ -29,7 +29,7 @@ class BackfillVersionAuthorAndCreatedAt < ActiveRecord::Migration[5.2]
issues = Issue.arel_table
projects = Project.arel_table
- Version.select(versions[:issue_id]).where(
+ select(versions[:issue_id]).where(
versions[:author_id].eq(nil).or(
versions[:created_at].eq(nil)
).and(
diff --git a/db/schema_migrations/20210427062807 b/db/schema_migrations/20210427062807
new file mode 100644
index 00000000000..c9d82dfa931
--- /dev/null
+++ b/db/schema_migrations/20210427062807
@@ -0,0 +1 @@
+306bb2bc3bfd20a57f1ac473e32596e7b7e7b6c2ae41c3fe5a7f45c551ce9207 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 653b329a067..a78b9656f28 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -22115,6 +22115,8 @@ CREATE INDEX index_badges_on_project_id ON badges USING btree (project_id);
CREATE INDEX index_batched_jobs_by_batched_migration_id_and_id ON batched_background_migration_jobs USING btree (batched_background_migration_id, id);
+CREATE INDEX index_batched_jobs_on_batched_migration_id_and_status ON batched_background_migration_jobs USING btree (batched_background_migration_id, status);
+
CREATE INDEX index_batched_migrations_on_job_table_and_column_name ON batched_background_migrations USING btree (job_class_name, table_name, column_name);
CREATE INDEX index_board_assignees_on_assignee_id ON board_assignees USING btree (assignee_id);
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index b1c19ef7526..fd5642b2c4b 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1122,6 +1122,35 @@ to define the explicit address that the GitLab Pages daemon should listen on:
gitlab_pages['listen_proxy'] = '127.0.0.1:8090'
```
+### Intermittent 502 errors or after a few days
+
+If you run Pages on a system that uses `systemd` and
+[`tmpfiles.d`](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html),
+you may encounter intermittent 502 errors trying to serve Pages with an error similar to:
+
+```plaintext
+dial tcp: lookup gitlab.example.com on [::1]:53: dial udp [::1]:53: connect: no route to host"
+```
+
+GitLab Pages creates a [bind mount](https://man7.org/linux/man-pages/man8/mount.8.html)
+inside `/tmp/gitlab-pages-*` that includes files like `/etc/hosts`.
+However, `systemd` may clean the `/tmp/` directory on a regular basis so the DNS
+configuration may be lost.
+
+To stop `systemd` from cleaning the Pages related content:
+
+1. Tell `tmpfiles.d` to not remove the Pages `/tmp` directory:
+
+ ```shell
+ echo 'x /tmp/gitlab-pages-*' >> /etc/tmpfiles.d/gitlab-pages-jail.conf
+ ```
+
+1. Restart GitLab Pages:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
+
### 404 error after transferring the project to a different group or user, or changing project path
If you encounter a `404 Not Found` error a Pages site after transferring a project to
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index dae38d0aea1..a9ab03c3213 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -676,7 +676,7 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
> - [Environment scoping for CI/CD variables was moved to all tiers](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) in GitLab 12.2.
-> - [Environment scoping for Group CI/CD variables](https://gitlab.com/gitlab-org/gitlab/-/issues/2874) added to GitLab Premium in 13.11
+> - [Environment scoping for Group CI/CD variables](https://gitlab.com/gitlab-org/gitlab/-/issues/2874) added to GitLab Premium in 13.11.
You can limit the environment scope of a CI/CD variable by
defining which environments it can be available for.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 9abd21c4d15..5796a5c6b7d 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -181,7 +181,7 @@ To add a group variable:
- **Key**: Must be one line, with no spaces, using only letters, numbers, or `_`.
- **Value**: No limitations.
- **Type**: [`File` or `Variable`](#cicd-variable-types).
- - **Environment scope** (optional): `All`, or specific [environments](#limit-the-environment-scope-of-a-cicd-variable). **PREMIUM**
+ - **Environment scope** (optional): `All`, or specific [environments](#limit-the-environment-scope-of-a-cicd-variable). **(PREMIUM)**
- **Protect variable** (Optional): If selected, the variable is only available
in pipelines that run on protected branches or tags.
- **Mask variable** (Optional): If selected, the variable's **Value** is masked
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 40457dbb533..491f65bd88a 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -33,7 +33,7 @@ compatible.
For GitLab.com, please take into consideration that regular migrations (under `db/migrate`)
are run before [Canary is deployed](https://gitlab.com/gitlab-com/gl-infra/readiness/-/tree/master/library/canary/#configuration-and-deployment),
-and post-deployment migrations (`db/post_migrate`) are run after the deployment to production has finished.
+and [post-deployment migrations](post_deployment_migrations.md) (`db/post_migrate`) are run after the deployment to production has finished.
## Schema Changes
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 80b0c3b1f29..da2e2e8dcc2 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -273,6 +273,10 @@ Sure. All you need to do is download the artifacts archive from the job page.
Yes. GitLab Pages doesn't care whether you set your project's visibility level
to private, internal or public.
+### Can I create a personal or a group website
+
+Yes. See the documentation about [GitLab Pages domain names, URLs, and base URLs](getting_started_part_one.md).
+
### Do I need to create a user/group website before creating a project website?
No, you don't. You can create your project first and access it under
diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb
index 8cedace0db3..869b97b8ac0 100644
--- a/lib/gitlab/database/background_migration/batched_job.rb
+++ b/lib/gitlab/database/background_migration/batched_job.rb
@@ -4,10 +4,23 @@ module Gitlab
module Database
module BackgroundMigration
class BatchedJob < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
+ include FromUnion
+
self.table_name = :batched_background_migration_jobs
+ MAX_ATTEMPTS = 3
+ STUCK_JOBS_TIMEOUT = 1.hour.freeze
+
belongs_to :batched_migration, foreign_key: :batched_background_migration_id
+ scope :active, -> { where(status: [:pending, :running]) }
+ scope :stuck, -> { active.where('updated_at <= ?', STUCK_JOBS_TIMEOUT.ago) }
+ scope :retriable, -> {
+ failed_jobs = where(status: :failed).where('attempts < ?', MAX_ATTEMPTS)
+
+ from_union([failed_jobs, self.stuck])
+ }
+
enum status: {
pending: 0,
running: 1,
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index 1203efd06a7..e85162f355e 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -20,7 +20,8 @@ module Gitlab
paused: 0,
active: 1,
aborted: 2,
- finished: 3
+ finished: 3,
+ failed: 4
}
attribute :pause_ms, :integer, default: 100
diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb
index 4e125431122..67fe6c536e6 100644
--- a/lib/gitlab/database/background_migration/batched_migration_runner.rb
+++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb
@@ -19,7 +19,7 @@ module Gitlab
#
# Note that this method is primarily intended to called by a scheduled worker.
def run_migration_job(active_migration)
- if next_batched_job = create_next_batched_job!(active_migration)
+ if next_batched_job = find_or_create_next_batched_job(active_migration)
migration_wrapper.perform(next_batched_job)
active_migration.optimize!
@@ -48,12 +48,12 @@ module Gitlab
attr_reader :migration_wrapper
- def create_next_batched_job!(active_migration)
- next_batch_range = find_next_batch_range(active_migration)
-
- return if next_batch_range.nil?
-
- active_migration.create_batched_job!(next_batch_range.min, next_batch_range.max)
+ def find_or_create_next_batched_job(active_migration)
+ if next_batch_range = find_next_batch_range(active_migration)
+ active_migration.create_batched_job!(next_batch_range.min, next_batch_range.max)
+ else
+ active_migration.batched_jobs.retriable.first
+ end
end
def find_next_batch_range(active_migration)
@@ -82,7 +82,13 @@ module Gitlab
end
def finish_active_migration(active_migration)
- active_migration.finished!
+ return if active_migration.batched_jobs.active.exists?
+
+ if active_migration.batched_jobs.failed.exists?
+ active_migration.failed!
+ else
+ active_migration.finished!
+ end
end
end
end
diff --git a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
index ede1fbca737..4851c611b5a 100644
--- a/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
+++ b/lib/gitlab/database/background_migration/batched_migration_wrapper.rb
@@ -31,7 +31,7 @@ module Gitlab
private
def start_tracking_execution(tracking_record)
- tracking_record.update!(attempts: tracking_record.attempts + 1, status: :running, started_at: Time.current)
+ tracking_record.update!(attempts: tracking_record.attempts + 1, status: :running, started_at: Time.current, finished_at: nil, metrics: {})
end
def execute_batch(tracking_record)
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 145bb6d7b8f..259e53c2f7c 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -34,10 +34,6 @@
module Gitlab
module Experimentation
EXPERIMENTS = {
- invite_members_version_b: {
- tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB',
- use_backwards_compatible_subject_index: true
- },
invite_members_empty_group_version_a: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA',
use_backwards_compatible_subject_index: true
diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb
index 9a74266693b..ee44740b58b 100644
--- a/lib/gitlab/object_hierarchy.rb
+++ b/lib/gitlab/object_hierarchy.rb
@@ -7,7 +7,7 @@ module Gitlab
class ObjectHierarchy
DEPTH_COLUMN = :depth
- attr_reader :ancestors_base, :descendants_base, :model, :options
+ attr_reader :ancestors_base, :descendants_base, :model, :options, :unscoped_model
# ancestors_base - An instance of ActiveRecord::Relation for which to
# get parent objects.
@@ -19,6 +19,7 @@ module Gitlab
@ancestors_base = ancestors_base
@descendants_base = descendants_base
@model = ancestors_base.model
+ @unscoped_model = @model.unscoped
@options = options
end
@@ -70,23 +71,23 @@ module Gitlab
# if hierarchy_order is given, the calculated `depth` should be present in SELECT
if expose_depth
- recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all).distinct
- read_only(model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)).order(depth: hierarchy_order))
+ recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all).distinct
+ read_only(unscoped_model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table)).order(depth: hierarchy_order))
else
- recursive_query = base_and_ancestors_cte(upto).apply_to(model.all)
+ recursive_query = base_and_ancestors_cte(upto).apply_to(unscoped_model.all)
if skip_ordering?
recursive_query = recursive_query.distinct
else
recursive_query = recursive_query.reselect(*recursive_query.arel.projections, 'ROW_NUMBER() OVER () as depth').distinct
- recursive_query = model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table))
+ recursive_query = unscoped_model.from(Arel::Nodes::As.new(recursive_query.arel, objects_table))
recursive_query = remove_depth_and_maintain_order(recursive_query, hierarchy_order: hierarchy_order)
end
read_only(recursive_query)
end
else
- recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all)
+ recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(unscoped_model.all)
recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order
read_only(recursive_query)
end
@@ -103,23 +104,23 @@ module Gitlab
if use_distinct?
# Always calculate `depth`, remove it later if with_depth is false
if with_depth
- base_cte = base_and_descendants_cte(with_depth: true).apply_to(model.all).distinct
- read_only(model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)).order(depth: :asc))
+ base_cte = base_and_descendants_cte(with_depth: true).apply_to(unscoped_model.all).distinct
+ read_only(unscoped_model.from(Arel::Nodes::As.new(base_cte.arel, objects_table)).order(depth: :asc))
else
- base_cte = base_and_descendants_cte.apply_to(model.all)
+ base_cte = base_and_descendants_cte.apply_to(unscoped_model.all)
if skip_ordering?
base_cte = base_cte.distinct
else
base_cte = base_cte.reselect(*base_cte.arel.projections, 'ROW_NUMBER() OVER () as depth').distinct
- base_cte = model.from(Arel::Nodes::As.new(base_cte.arel, objects_table))
+ base_cte = unscoped_model.from(Arel::Nodes::As.new(base_cte.arel, objects_table))
base_cte = remove_depth_and_maintain_order(base_cte, hierarchy_order: :asc)
end
read_only(base_cte)
end
else
- read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(model.all))
+ read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(unscoped_model.all))
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -154,16 +155,15 @@ module Gitlab
ancestors_table = ancestors.alias_to(objects_table)
descendants_table = descendants.alias_to(objects_table)
- ancestors_scope = model.unscoped.from(ancestors_table)
- descendants_scope = model.unscoped.from(descendants_table)
+ ancestors_scope = unscoped_model.from(ancestors_table)
+ descendants_scope = unscoped_model.from(descendants_table)
if use_distinct?
ancestors_scope = ancestors_scope.distinct
descendants_scope = descendants_scope.distinct
end
- relation = model
- .unscoped
+ relation = unscoped_model
.with
.recursive(ancestors.to_arel, descendants.to_arel)
.from_union([
@@ -215,7 +215,7 @@ module Gitlab
cte << base_query
# Recursively get all the ancestors of the base set.
- parent_query = model
+ parent_query = unscoped_model
.from(from_tables(cte))
.where(ancestor_conditions(cte))
.except(:order)
@@ -248,7 +248,7 @@ module Gitlab
cte << base_query
# Recursively get all the descendants of the base set.
- descendants_query = model
+ descendants_query = unscoped_model
.from(from_tables(cte))
.where(descendant_conditions(cte))
.except(:order)
diff --git a/lib/sidebars/projects/menus/ci_cd_menu.rb b/lib/sidebars/projects/menus/ci_cd_menu.rb
new file mode 100644
index 00000000000..c0336d8dfbc
--- /dev/null
+++ b/lib/sidebars/projects/menus/ci_cd_menu.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ class CiCdMenu < ::Sidebars::Menu
+ override :configure_menu_items
+ def configure_menu_items
+ return unless can?(context.current_user, :read_build, context.project)
+
+ add_item(pipelines_menu_item)
+ add_item(pipelines_editor_menu_item)
+ add_item(jobs_menu_item)
+ add_item(artifacts_menu_item)
+ add_item(pipeline_schedules_menu_item)
+ end
+
+ override :link
+ def link
+ project_pipelines_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-pipelines rspec-link-pipelines'
+ }
+ end
+
+ override :title
+ def title
+ _('CI/CD')
+ end
+
+ override :title_html_options
+ def title_html_options
+ {
+ id: 'js-onboarding-pipelines-link'
+ }
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'rocket'
+ end
+
+ private
+
+ def pipelines_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Pipelines'),
+ link: project_pipelines_path(context.project),
+ container_html_options: { class: 'shortcuts-pipelines' },
+ active_routes: { path: pipelines_routes },
+ item_id: :pipelines
+ )
+ end
+
+ def pipelines_routes
+ %w[
+ pipelines#index
+ pipelines#show
+ pipelines#new
+ ]
+ end
+
+ def pipelines_editor_menu_item
+ return unless context.can_view_pipeline_editor
+
+ ::Sidebars::MenuItem.new(
+ title: s_('Pipelines|Editor'),
+ link: project_ci_pipeline_editor_path(context.project),
+ active_routes: { path: 'projects/ci/pipeline_editor#show' },
+ item_id: :pipelines_editor
+ )
+ end
+
+ def jobs_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Jobs'),
+ link: project_jobs_path(context.project),
+ container_html_options: { class: 'shortcuts-builds' },
+ active_routes: { controller: :jobs },
+ item_id: :jobs
+ )
+ end
+
+ def artifacts_menu_item
+ return unless Feature.enabled?(:artifacts_management_page, context.project)
+
+ ::Sidebars::MenuItem.new(
+ title: _('Artifacts'),
+ link: project_artifacts_path(context.project),
+ container_html_options: { class: 'shortcuts-builds' },
+ active_routes: { path: 'artifacts#index' },
+ item_id: :artifacts
+ )
+ end
+
+ def pipeline_schedules_menu_item
+ ::Sidebars::MenuItem.new(
+ title: _('Schedules'),
+ link: pipeline_schedules_path(context.project),
+ container_html_options: { class: 'shortcuts-builds' },
+ active_routes: { controller: :pipeline_schedules },
+ item_id: :pipeline_schedules
+ )
+ end
+ end
+ end
+ end
+end
+
+Sidebars::Projects::Menus::CiCdMenu.prepend_if_ee('EE::Sidebars::Projects::Menus::CiCdMenu')
diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb
index 1db4d55740a..4cafc530979 100644
--- a/lib/sidebars/projects/panel.rb
+++ b/lib/sidebars/projects/panel.rb
@@ -14,6 +14,7 @@ module Sidebars
add_menu(Sidebars::Projects::Menus::ExternalIssueTrackerMenu.new(context))
add_menu(Sidebars::Projects::Menus::LabelsMenu.new(context))
add_menu(Sidebars::Projects::Menus::MergeRequestsMenu.new(context))
+ add_menu(Sidebars::Projects::Menus::CiCdMenu.new(context))
end
override :render_raw_menus_partial
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 20f631e5d97..00c749e85d5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6032,9 +6032,6 @@ msgstr ""
msgid "Changes to the title have not been saved"
msgstr ""
-msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
-msgstr ""
-
msgid "Changing group URL can have unintended side effects."
msgstr ""
@@ -16252,15 +16249,9 @@ msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr ""
-msgid "How many replicas each Elasticsearch shard has."
-msgstr ""
-
msgid "How many seconds an IP will be counted towards the limit"
msgstr ""
-msgid "How many shards to split the Elasticsearch index over."
-msgstr ""
-
msgid "How many users will be evaluating the trial?"
msgstr ""
@@ -17257,6 +17248,9 @@ msgstr ""
msgid "Indent"
msgstr ""
+msgid "Index"
+msgstr ""
+
msgid "Index all projects"
msgstr ""
@@ -17871,24 +17865,12 @@ msgstr ""
msgid "InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later."
msgstr ""
-msgid "InviteMember|Oops, this feature isn't ready yet"
-msgstr ""
-
-msgid "InviteMember|See who can invite members for you"
-msgstr ""
-
msgid "InviteMember|Send invitations"
msgstr ""
msgid "InviteMember|Skip this for now"
msgstr ""
-msgid "InviteMember|Until then, ask an owner to invite new project members for you"
-msgstr ""
-
-msgid "InviteMember|We're working to allow everyone to invite new members, making it easier for teams to get started with GitLab"
-msgstr ""
-
msgid "InviteReminderEmail|%{inviter} is still waiting for you to join GitLab"
msgstr ""
@@ -19001,6 +18983,9 @@ msgstr ""
msgid "Learn more about group-level project templates"
msgstr ""
+msgid "Learn more about shards and replicas in the %{configuration_link_start}Advanced search configuration%{configuration_link_end} documentation. Changes won't take place until the index is %{recreated_link_start}recreated%{recreated_link_end}."
+msgstr ""
+
msgid "Learn more about signing commits"
msgstr ""
@@ -22316,10 +22301,7 @@ msgstr ""
msgid "Number of %{itemTitle}"
msgstr ""
-msgid "Number of Elasticsearch replicas"
-msgstr ""
-
-msgid "Number of Elasticsearch shards"
+msgid "Number of Elasticsearch shards and replicas (per index)"
msgstr ""
msgid "Number of Git pushes after which 'git gc' is run."
@@ -22358,6 +22340,12 @@ msgstr ""
msgid "Number of files touched"
msgstr ""
+msgid "Number of replicas"
+msgstr ""
+
+msgid "Number of shards"
+msgstr ""
+
msgid "OK"
msgstr ""
diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb
index 398712c04d2..7cb2fd6c655 100644
--- a/qa/qa/page/project/sub_menus/ci_cd.rb
+++ b/qa/qa/page/project/sub_menus/ci_cd.rb
@@ -12,16 +12,12 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
-
- view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
- element :link_pipelines
- end
end
end
def click_ci_cd_pipelines
within_sidebar do
- click_element :link_pipelines
+ click_element(:sidebar_menu_link, menu_item: 'CI/CD')
end
end
end
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index 4bd04eabe69..2359765fedd 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -57,7 +57,6 @@ module DeprecationToolkitEnv
%w[
activerecord-6.0.3.6/lib/active_record/migration.rb
activesupport-6.0.3.6/lib/active_support/cache.rb
- carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb
activerecord-6.0.3.6/lib/active_record/relation.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
]
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 04b4caa52fe..0566ce968d2 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -130,30 +130,7 @@ RSpec.describe 'Issue Sidebar' do
end
end
- context 'when invite_members_version_b experiment is enabled' do
- before do
- stub_experiment_for_subject(invite_members_version_b: true)
- end
-
- it 'shows a link for inviting members and follows through to modal' do
- project.add_developer(user)
- visit_issue(project, issue2)
-
- open_assignees_dropdown
-
- page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite members', href: '#')
- expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]')
- expect(page).to have_selector('[data-track-label="edit_assignee"]')
- end
-
- click_link 'Invite members'
-
- expect(page).to have_content("Oops, this feature isn't ready yet")
- end
- end
-
- context 'when invite_members_version_b experiment is disabled' do
+ context 'when user cannot invite members in assignee dropdown' do
it 'shows author in assignee dropdown and no invite link' do
project.add_developer(user)
visit_issue(project, issue2)
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 8417ac9a41a..3758723c571 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -9,7 +9,7 @@ import {
formatIssue,
getMoveData,
} from '~/boards/boards_util';
-import { inactiveId, ISSUABLE, ListType } from '~/boards/constants';
+import { inactiveId, ISSUABLE, ListType, issuableTypes } from '~/boards/constants';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import actions, { gqlClient } from '~/boards/stores/actions';
@@ -459,7 +459,7 @@ describe('updateList', () => {
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
- issuableType: 'issue',
+ issuableType: issuableTypes.issue,
};
testAction(
@@ -503,6 +503,7 @@ describe('removeList', () => {
beforeEach(() => {
state = {
boardLists: mockListsById,
+ issuableType: issuableTypes.issue,
};
});
@@ -1375,7 +1376,7 @@ describe('setActiveItemSubscribed', () => {
[mockActiveIssue.id]: mockActiveIssue,
},
fullPath: 'gitlab-org',
- issuableType: 'issue',
+ issuableType: issuableTypes.issue,
};
const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false };
const subscribedState = true;
@@ -1483,7 +1484,7 @@ describe('setActiveIssueMilestone', () => {
describe('setActiveItemTitle', () => {
const state = {
boardItems: { [mockIssue.id]: mockIssue },
- issuableType: 'issue',
+ issuableType: issuableTypes.issue,
fullPath: 'path/f',
};
const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
diff --git a/spec/frontend/invite_member/components/invite_member_modal_spec.js b/spec/frontend/invite_member/components/invite_member_modal_spec.js
deleted file mode 100644
index 03e3da2d5ef..00000000000
--- a/spec/frontend/invite_member/components/invite_member_modal_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { GlLink, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { stubComponent } from 'helpers/stub_component';
-import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
-import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
-
-const memberPath = 'member_path';
-
-const GlEmoji = { template: '<img />' };
-const createComponent = () => {
- return shallowMount(InviteMemberModal, {
- propsData: {
- membersPath: memberPath,
- },
- stubs: {
- GlEmoji,
- GlModal: stubComponent(GlModal, {
- template: '<div><slot name="modal-title"></slot><slot></slot></div>',
- }),
- },
- });
-};
-
-describe('InviteMemberModal', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findLink = () => wrapper.find(GlLink);
-
- describe('rendering the modal', () => {
- it('renders the modal with the correct title', () => {
- expect(wrapper.text()).toContain("Oops, this feature isn't ready yet");
- });
-
- describe('rendering the see who link', () => {
- it('renders the correct link', () => {
- expect(findLink().attributes('href')).toBe(memberPath);
- });
- });
- });
-
- describe('tracking', () => {
- let trackingSpy;
-
- afterEach(() => {
- unmockTracking();
- });
-
- it('send an event when go to pipelines is clicked', () => {
- trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- triggerEvent(findLink().element);
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_who_can_invite_link', {
- label: 'invite_members_message',
- });
- });
- });
-});
diff --git a/spec/frontend/invite_member/components/invite_member_trigger_mock_data.js b/spec/frontend/invite_member/components/invite_member_trigger_mock_data.js
deleted file mode 100644
index 9b34a8027e9..00000000000
--- a/spec/frontend/invite_member/components/invite_member_trigger_mock_data.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const triggerProvides = {
- displayText: 'Invite member',
- event: 'click_invite_members_version_b',
- label: 'edit_assignee',
-};
-
-export default triggerProvides;
diff --git a/spec/frontend/invite_member/components/invite_member_trigger_spec.js b/spec/frontend/invite_member/components/invite_member_trigger_spec.js
deleted file mode 100644
index 630e2dbfc16..00000000000
--- a/spec/frontend/invite_member/components/invite_member_trigger_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
-import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
-import triggerProvides from './invite_member_trigger_mock_data';
-
-const createComponent = () => {
- return shallowMount(InviteMemberTrigger, { propsData: triggerProvides });
-};
-
-describe('InviteMemberTrigger', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findLink = () => wrapper.find(GlLink);
-
- describe('displayText', () => {
- it('includes the correct displayText for the link', () => {
- expect(findLink().text()).toBe(triggerProvides.displayText);
- });
- });
-
- describe('tracking', () => {
- let trackingSpy;
-
- afterEach(() => {
- unmockTracking();
- });
-
- it('send an event when go to pipelines is clicked', () => {
- trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- triggerEvent(findLink().element);
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', triggerProvides.event, {
- label: triggerProvides.label,
- });
- });
- });
-});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 543bc1c128a..2973c25b936 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -533,7 +533,7 @@ describe('Sidebar assignees widget', () => {
expect(findInviteMembersLink().exists()).toBe(false);
});
- it('does not render invite members link if `directlyInviteMembers` and `indirectlyInviteMembers` were not passed', async () => {
+ it('does not render invite members link if `directlyInviteMembers` was not passed', async () => {
createComponent();
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(false);
@@ -548,14 +548,4 @@ describe('Sidebar assignees widget', () => {
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(true);
});
-
- it('renders invite members link if `indirectlyInviteMembers` is true', async () => {
- createComponent({
- provide: {
- indirectlyInviteMembers: true,
- },
- });
- await waitForPromises();
- expect(findInviteMembersLink().exists()).toBe(true);
- });
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
index 06f7da3d1ab..cfbe7227915 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -1,25 +1,14 @@
import { shallowMount } from '@vue/test-utils';
-import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
-import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_members.vue';
-const testProjectMembersPath = 'test-path';
-
describe('Sidebar invite members component', () => {
let wrapper;
const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger);
- const findIndirectInviteLink = () => wrapper.findComponent(InviteMemberTrigger);
- const findInviteModal = () => wrapper.findComponent(InviteMemberModal);
- const createComponent = ({ directlyInviteMembers = false } = {}) => {
- wrapper = shallowMount(SidebarInviteMembers, {
- provide: {
- directlyInviteMembers,
- projectMembersPath: testProjectMembersPath,
- },
- });
+ const createComponent = () => {
+ wrapper = shallowMount(SidebarInviteMembers);
};
afterEach(() => {
@@ -28,32 +17,11 @@ describe('Sidebar invite members component', () => {
describe('when directly inviting members', () => {
beforeEach(() => {
- createComponent({ directlyInviteMembers: true });
+ createComponent();
});
it('renders a direct link to project members path', () => {
expect(findDirectInviteLink().exists()).toBe(true);
});
-
- it('does not render invite members trigger and modal components', () => {
- expect(findIndirectInviteLink().exists()).toBe(false);
- expect(findInviteModal().exists()).toBe(false);
- });
- });
-
- describe('when indirectly inviting members', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('does not render a direct link to project members path', () => {
- expect(findDirectInviteLink().exists()).toBe(false);
- });
-
- it('does not render invite members trigger and modal components', () => {
- expect(findIndirectInviteLink().exists()).toBe(true);
- expect(findInviteModal().exists()).toBe(true);
- expect(findInviteModal().props('membersPath')).toBe(testProjectMembersPath);
- });
});
});
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 89bbbba9913..c5adbe9bb09 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -1,7 +1,7 @@
-import { waitFor } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
import { memoize, cloneDeep } from 'lodash';
import { getFixture, getJSONFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import UsersSelect from '~/users_select';
@@ -103,8 +103,10 @@ export const setAssignees = (...users) => {
);
};
export const toggleDropdown = () => findUserSearchButton().click();
-export const waitForDropdownItems = () =>
- waitFor(() => expect(findDropdownItem(getUsersFixtureAt(0))).not.toBeNull());
+export const waitForDropdownItems = async () => {
+ await axios.waitForAll();
+ await waitForPromises();
+};
// assertion helpers ---------------------------------------------------------
export const createUnassignedExpectation = () => {
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 7ddf7d059e5..dbe4f970a99 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -12,21 +12,6 @@ RSpec.describe InviteMembersHelper do
helper.extend(Gitlab::Experimentation::ControllerConcern)
end
- describe '#show_invite_members_track_event' do
- it 'shows values when can directly invite members' do
- allow(helper).to receive(:directly_invite_members?).and_return(true)
-
- expect(helper.show_invite_members_track_event).to eq 'show_invite_members'
- end
-
- it 'shows values when can indirectly invite members' do
- allow(helper).to receive(:directly_invite_members?).and_return(false)
- allow(helper).to receive(:indirectly_invite_members?).and_return(true)
-
- expect(helper.show_invite_members_track_event).to eq 'show_invite_members_version_b'
- end
- end
-
context 'with project' do
before do
assign(:project, project)
@@ -87,38 +72,6 @@ RSpec.describe InviteMembersHelper do
end
end
end
-
- describe "#indirectly_invite_members?" do
- context 'when a user is a developer' do
- before do
- allow(helper).to receive(:current_user) { developer }
- end
-
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false }
-
- expect(helper.indirectly_invite_members?).to eq false
- end
-
- it 'returns true' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
-
- expect(helper.indirectly_invite_members?).to eq true
- end
- end
-
- context 'when a user is an owner' do
- before do
- allow(helper).to receive(:current_user) { owner }
- end
-
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
-
- expect(helper.indirectly_invite_members?).to eq false
- end
- end
- end
end
context 'with group' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index abee1fec80a..78e0b7627e9 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -9,6 +9,42 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
it { is_expected.to belong_to(:batched_migration).with_foreign_key(:batched_background_migration_id) }
end
+ describe 'scopes' do
+ let_it_be(:fixed_time) { Time.new(2021, 04, 27, 10, 00, 00, 00) }
+
+ let_it_be(:pending_job) { create(:batched_background_migration_job, status: :pending, updated_at: fixed_time) }
+ let_it_be(:running_job) { create(:batched_background_migration_job, status: :running, updated_at: fixed_time) }
+ let_it_be(:stuck_job) { create(:batched_background_migration_job, status: :pending, updated_at: fixed_time - described_class::STUCK_JOBS_TIMEOUT) }
+ let_it_be(:failed_job) { create(:batched_background_migration_job, status: :failed, attempts: 1) }
+
+ before_all do
+ create(:batched_background_migration_job, status: :failed, attempts: described_class::MAX_ATTEMPTS)
+ create(:batched_background_migration_job, status: :succeeded)
+ end
+
+ before do
+ travel_to fixed_time
+ end
+
+ describe '.active' do
+ it 'returns active jobs' do
+ expect(described_class.active).to contain_exactly(pending_job, running_job, stuck_job)
+ end
+ end
+
+ describe '.stuck' do
+ it 'returns stuck jobs' do
+ expect(described_class.stuck).to contain_exactly(stuck_job)
+ end
+ end
+
+ describe '.retriable' do
+ it 'returns retriable jobs' do
+ expect(described_class.retriable).to contain_exactly(failed_job, stuck_job)
+ end
+ end
+ end
+
describe 'delegated batched_migration attributes' do
let(:batched_job) { build(:batched_background_migration_job) }
let(:batched_migration) { batched_job.batched_migration }
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
index 79b21172dc6..9f0493ab0d7 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb
@@ -17,9 +17,9 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
end
it 'marks the migration as finished' do
- relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: migration.id)
+ runner.run_migration_job(migration)
- expect { runner.run_migration_job(migration) }.to change { relation.count }.by(1)
+ expect(migration.reload).to be_finished
end
end
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
let!(:event3) { create(:event) }
let!(:migration) do
- create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event3.id)
+ create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event2.id)
end
let!(:previous_job) do
@@ -101,14 +101,24 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
min_value: event1.id,
max_value: event2.id,
batch_size: 2,
- sub_batch_size: 1)
+ sub_batch_size: 1,
+ status: :succeeded
+ )
end
let(:job_relation) do
Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id)
end
+ context 'when the migration has no batches remaining' do
+ it_behaves_like 'it has completed the migration'
+ end
+
context 'when the migration has batches to process' do
+ before do
+ migration.update!(max_value: event3.id)
+ end
+
it 'runs the migration job for the next batch' do
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.last)
@@ -132,17 +142,82 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
end
end
- context 'when the migration has no batches remaining' do
+ context 'when migration has failed jobs' do
before do
- create(:batched_background_migration_job,
- batched_migration: migration,
- min_value: event3.id,
- max_value: event3.id,
- batch_size: 2,
- sub_batch_size: 1)
+ previous_job.update!(status: :failed)
end
- it_behaves_like 'it has completed the migration'
+ it 'retries the failed job' do
+ expect(migration_wrapper).to receive(:perform) do |job_record|
+ expect(job_record).to eq(previous_job)
+ end
+
+ expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(0)
+ end
+
+ context 'when failed job has reached the maximum number of attempts' do
+ before do
+ previous_job.update!(attempts: Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS)
+ end
+
+ it 'marks the migration as failed' do
+ expect(migration_wrapper).not_to receive(:perform)
+
+ expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(0)
+
+ expect(migration).to be_failed
+ end
+ end
+ end
+
+ context 'when migration has stuck jobs' do
+ before do
+ previous_job.update!(status: :running, updated_at: 1.hour.ago - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT)
+ end
+
+ it 'retries the stuck job' do
+ expect(migration_wrapper).to receive(:perform) do |job_record|
+ expect(job_record).to eq(previous_job)
+ end
+
+ expect { runner.run_migration_job(migration.reload) }.to change { job_relation.count }.by(0)
+ end
+ end
+
+ context 'when migration has possible stuck jobs' do
+ before do
+ previous_job.update!(status: :running, updated_at: 1.hour.from_now - Gitlab::Database::BackgroundMigration::BatchedJob::STUCK_JOBS_TIMEOUT)
+ end
+
+ it 'keeps the migration active' do
+ expect(migration_wrapper).not_to receive(:perform)
+
+ expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(0)
+
+ expect(migration.reload).to be_active
+ end
+ end
+
+ context 'when the migration has batches to process and failed jobs' do
+ before do
+ migration.update!(max_value: event3.id)
+ previous_job.update!(status: :failed)
+ end
+
+ it 'runs next batch then retries the failed job' do
+ expect(migration_wrapper).to receive(:perform) do |job_record|
+ expect(job_record).to eq(job_relation.last)
+ job_record.update!(status: :succeeded)
+ end
+
+ expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1)
+
+ expect(migration_wrapper).to receive(:perform) do |job_record|
+ expect(job_record).to eq(previous_job)
+ end
+
+ expect { runner.run_migration_job(migration.reload) }.to change { job_relation.count }.by(0)
+ end
end
end
end
@@ -189,10 +264,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
it 'runs all jobs inline until finishing the migration' do
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.first)
+ job_record.update!(status: :succeeded)
end
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.last)
+ job_record.update!(status: :succeeded)
end
expect { runner.run_entire_migration(migration) }.to change { job_relation.count }.by(2)
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index fdbc2286502..987f2c5a935 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -49,6 +49,42 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
end
end
+ context 'when running a job that failed previously' do
+ let!(:job_record) do
+ create(:batched_background_migration_job,
+ batched_migration: active_migration,
+ pause_ms: pause_ms,
+ attempts: 1,
+ status: :failed,
+ finished_at: 1.hour.ago,
+ metrics: { 'my_metrics' => 'some_value' }
+ )
+ end
+
+ it 'increments attempts and updates other fields' do
+ updated_metrics = { 'updated_metrics' => 'some_value' }
+
+ expect(job_instance).to receive(:perform)
+ expect(job_instance).to receive(:batch_metrics).and_return(updated_metrics)
+
+ expect(job_record).to receive(:update!).with(
+ hash_including(attempts: 2, status: :running, finished_at: nil, metrics: {})
+ ).and_call_original
+
+ freeze_time do
+ subject
+
+ job_record.reload
+
+ expect(job_record).not_to be_failed
+ expect(job_record.attempts).to eq(2)
+ expect(job_record.started_at).to eq(Time.current)
+ expect(job_record.finished_at).to eq(Time.current)
+ expect(job_record.metrics).to eq(updated_metrics)
+ end
+ end
+ end
+
context 'reporting prometheus metrics' do
let(:labels) { job_record.batched_migration.prometheus_labels }
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 5fef14bd2a0..10bfa9e8d0e 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -7,7 +7,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Experimentation::EXPERIMENTS do
it 'temporarily ensures we know what experiments exist for backwards compatibility' do
expected_experiment_keys = [
- :invite_members_version_b,
:invite_members_empty_group_version_a,
:contact_sales_btn_in_app
]
diff --git a/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
new file mode 100644
index 00000000000..89b03e1c918
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::CiCdMenu do
+ let(:project) { build(:project) }
+ let(:user) { project.owner }
+ let(:can_view_pipeline_editor) { true }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: 'master', can_view_pipeline_editor: can_view_pipeline_editor) }
+
+ subject { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when user cannot read builds' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(subject.render?).to eq false
+ end
+ end
+
+ context 'when user can read builds' do
+ it 'returns true' do
+ expect(subject.render?).to eq true
+ end
+ end
+ end
+
+ describe 'Pipelines Editor' do
+ subject { described_class.new(context).items.index { |e| e.item_id == :pipelines_editor } }
+
+ context 'when user cannot view pipeline editor' do
+ let(:can_view_pipeline_editor) { false }
+
+ it 'does not include pipeline editor menu item' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when user can view pipeline editor' do
+ it 'includes pipeline editor menu item' do
+ is_expected.not_to be_nil
+ end
+ end
+ end
+
+ describe 'Artifacts' do
+ subject { described_class.new(context).items.index { |e| e.item_id == :artifacts } }
+
+ context 'when feature flag :artifacts_management_page is disabled' do
+ it 'does not include artifacts menu item' do
+ stub_feature_flags(artifacts_management_page: false)
+
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when feature flag :artifacts_management_page is enabled' do
+ it 'includes artifacts menu item' do
+ stub_feature_flags(artifacts_management_page: true)
+
+ is_expected.not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/integration_spec.rb b/spec/models/concerns/integration_spec.rb
index 781e2aece56..781e2aece56 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/concerns/integration_spec.rb
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index 49c3674277d..736c353c2aa 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -22,32 +22,7 @@ RSpec.shared_examples 'issuable invite members experiments' do
end
end
- context 'when invite_members_version_b experiment is enabled' do
- before do
- stub_experiment_for_subject(invite_members_version_b: true)
- end
-
- it 'shows a link for inviting members and follows through to modal' do
- project.add_developer(user)
- visit issuable_path
-
- find('.block.assignee .edit-link').click
-
- wait_for_requests
-
- page.within '.dropdown-menu-user' do
- expect(page).to have_link('Invite Members', href: '#')
- expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]')
- expect(page).to have_selector('[data-track-label="edit_assignee"]')
- end
-
- click_link 'Invite Members'
-
- expect(page).to have_content("Oops, this feature isn't ready yet")
- end
- end
-
- context 'when invite_members_version_b experiment is disabled' do
+ context 'when user cannot invite members in assignee dropdown' do
it 'shows author in assignee dropdown and no invite link' do
project.add_developer(user)
visit issuable_path
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index c501c418466..16362aed1cd 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -257,6 +257,64 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
+ describe 'CI/CD' do
+ it 'has a link to pipelines page' do
+ render
+
+ expect(rendered).to have_link('CI/CD', href: project_pipelines_path(project))
+ end
+
+ describe 'Artifacts' do
+ it 'has a link to the artifacts page' do
+ render
+
+ expect(rendered).to have_link('Artifacts', href: project_artifacts_path(project))
+ end
+ end
+
+ describe 'Jobs' do
+ it 'has a link to the jobs page' do
+ render
+
+ expect(rendered).to have_link('Jobs', href: project_jobs_path(project))
+ end
+ end
+
+ describe 'Pipeline Schedules' do
+ it 'has a link to the pipeline schedules page' do
+ render
+
+ expect(rendered).to have_link('Schedules', href: pipeline_schedules_path(project))
+ end
+ end
+
+ describe 'Pipelines' do
+ it 'has a link to the pipelines page' do
+ render
+
+ expect(rendered).to have_link('Pipelines', href: project_pipelines_path(project))
+ end
+ end
+
+ describe 'Pipeline Editor' do
+ it 'has a link to the pipeline editor' do
+ render
+
+ expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project))
+ end
+
+ context 'when user cannot access pipeline editor' do
+ it 'does not has a link to the pipeline editor' do
+ allow(view).to receive(:can_view_pipeline_editor?).and_return(false)
+
+ render
+
+ expect(rendered).not_to have_link('Editor', href: project_ci_pipeline_editor_path(project))
+ end
+ end
+ end
+ end
+
describe 'packages tab' do
before do
stub_container_registry_config(enabled: true)
@@ -419,48 +477,6 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'ci/cd settings tab' do
- before do
- project.update!(archived: project_archived)
- end
-
- context 'when project is archived' do
- let(:project_archived) { true }
-
- it 'does not show the ci/cd settings tab' do
- render
-
- expect(rendered).not_to have_link('CI/CD', href: project_settings_ci_cd_path(project))
- end
- end
-
- context 'when project is active' do
- let(:project_archived) { false }
-
- it 'shows the ci/cd settings tab' do
- render
-
- expect(rendered).to have_link('CI/CD', href: project_settings_ci_cd_path(project))
- end
- end
- end
-
- describe 'pipeline editor link' do
- it 'shows the pipeline editor link' do
- render
-
- expect(rendered).to have_link('Editor', href: project_ci_pipeline_editor_path(project))
- end
-
- it 'does not show the pipeline editor link' do
- allow(view).to receive(:can_view_pipeline_editor?).and_return(false)
-
- render
-
- expect(rendered).not_to have_link('Editor', href: project_ci_pipeline_editor_path(project))
- end
- end
-
describe 'operations settings tab' do
describe 'archive projects' do
before do