diff options
89 files changed, 832 insertions, 448 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 8a09d3eb4b0..394bbc2f633 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -804,26 +804,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/controllers/dashboard/projects_controller_spec.rb - spec/controllers/invites_controller_spec.rb - spec/controllers/profiles/emails_controller_spec.rb - - spec/controllers/projects/alerting/notifications_controller_spec.rb - - spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb - - spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb - - spec/controllers/projects/environments_controller_spec.rb - - spec/controllers/projects/feature_flags_controller_spec.rb - - spec/controllers/projects/incidents_controller_spec.rb - - spec/controllers/projects/issues_controller_spec.rb - - spec/controllers/projects/jobs_controller_spec.rb - - spec/controllers/projects/merge_requests_controller_spec.rb - - spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb - - spec/controllers/projects/pipelines_controller_spec.rb - - spec/controllers/projects/pipelines_settings_controller_spec.rb - - spec/controllers/projects/raw_controller_spec.rb - - spec/controllers/projects/releases/evidences_controller_spec.rb - - spec/controllers/projects/releases_controller_spec.rb - - spec/controllers/projects/settings/ci_cd_controller_spec.rb - - spec/controllers/projects/settings/operations_controller_spec.rb - - spec/controllers/projects/static_site_editor_controller_spec.rb - - spec/controllers/projects/todos_controller_spec.rb - - spec/controllers/projects_controller_spec.rb - spec/controllers/users/terms_controller_spec.rb - spec/features/boards/multiple_boards_spec.rb - spec/features/boards/sidebar_assignee_spec.rb @@ -842,8 +822,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/features/file_uploads/group_import_spec.rb - spec/features/file_uploads/project_import_spec.rb - spec/features/file_uploads/user_avatar_spec.rb - - spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb - - spec/features/groups_spec.rb - spec/features/issues/user_sees_breadcrumb_links_spec.rb - spec/features/markdown/metrics_spec.rb - spec/features/merge_request/user_creates_merge_request_spec.rb diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9a8bd7a1eb2..c2ce5930415 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -22c9b23bfcdeedc373462a54cea5ee55790a62b1 +408a10ac19698a289f4ee1e2a44ddaba31630112 diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 9639ca95cca..a4b1e6adacf 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -18,6 +18,8 @@ export default { ? BoardColumn : BoardColumnDeprecated, BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'), + EpicBoardContentSidebar: () => + import('ee_component/boards/components/epic_board_content_sidebar.vue'), EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), GlAlert, }, @@ -133,7 +135,14 @@ export default { <board-content-sidebar v-if="isSwimlanesOn || glFeatures.graphqlBoardLists" - class="issue-boards-sidebar" + class="boards-sidebar" + data-testid="issue-boards-sidebar" + /> + + <epic-board-content-sidebar + v-else-if="isEpicBoard" + class="boards-sidebar" + data-testid="epic-boards-sidebar" /> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index d74057e99bc..21b14124f59 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -34,7 +34,7 @@ export default { computed: { ...mapGetters([ 'isSidebarOpen', - 'activeIssue', + 'activeBoardItem', 'groupPathForActiveIssue', 'projectPathForActiveIssue', ]), @@ -46,13 +46,13 @@ export default { return this.isIssuableSidebar && this.isSidebarOpen; }, fullPath() { - return this.activeIssue?.referencePath?.split('#')[0] || ''; + return this.activeBoardItem?.referencePath?.split('#')[0] || ''; }, }, methods: { ...mapActions(['toggleBoardItem', 'setAssignees']), handleClose() { - this.toggleBoardItem({ boardItem: this.activeIssue, sidebarType: this.sidebarType }); + this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType }); }, }, }; @@ -61,7 +61,6 @@ export default { <template> <gl-drawer v-if="showSidebar" - data-testid="sidebar-drawer" :open="isSidebarOpen" :header-height="$options.headerHeight" @close="handleClose" @@ -70,9 +69,9 @@ export default { <template #default> <board-sidebar-issue-title /> <sidebar-assignees-widget - :iid="activeIssue.iid" + :iid="activeBoardItem.iid" :full-path="fullPath" - :initial-assignees="activeIssue.assignees" + :initial-assignees="activeBoardItem.assignees" class="assignee" @assignees-updated="setAssignees" /> @@ -80,7 +79,7 @@ export default { <div> <board-sidebar-milestone-select /> <sidebar-iteration-widget - :iid="activeIssue.iid" + :iid="activeBoardItem.iid" :workspace-path="projectPathForActiveIssue" :iterations-workspace-path="groupPathForActiveIssue" :issuable-type="issuableType" diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index e5eec70c544..78da4137d69 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -127,7 +127,7 @@ export default { if (this.isDeleteForm) { return 'danger'; } - return 'info'; + return 'confirm'; }, title() { if (this.readonly) { diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue index c6836efc32d..13e1e232676 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue @@ -18,16 +18,16 @@ export default { }; }, computed: { - ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), + ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']), hasDueDate() { - return this.activeIssue.dueDate != null; + return this.activeBoardItem.dueDate != null; }, parsedDueDate() { if (!this.hasDueDate) { return null; } - return parsePikadayDate(this.activeIssue.dueDate); + return parsePikadayDate(this.activeBoardItem.dueDate); }, formattedDueDate() { if (!this.hasDueDate) { diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue index 95864bd62a7..0aa65a30b46 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue @@ -27,7 +27,7 @@ export default { }; }, computed: { - ...mapGetters({ issue: 'activeIssue' }), + ...mapGetters({ issue: 'activeBoardItem' }), pendingChangesStorageKey() { return this.getPendingChangesKey(this.issue); }, diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue index 55b1596ee18..f78be83cd82 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue @@ -21,9 +21,9 @@ export default { }; }, computed: { - ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), + ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']), selectedLabels() { - const { labels = [] } = this.activeIssue; + const { labels = [] } = this.activeBoardItem; return labels.map((label) => ({ ...label, @@ -31,7 +31,7 @@ export default { })); }, issueLabels() { - const { labels = [] } = this.activeIssue; + const { labels = [] } = this.activeBoardItem; return labels.map((label) => ({ ...label, @@ -40,7 +40,7 @@ export default { }, }, methods: { - ...mapActions(['setActiveIssueLabels']), + ...mapActions(['setActiveBoardItemLabels']), async setLabels(payload) { this.loading = true; this.$refs.sidebarItem.collapse(); @@ -52,7 +52,7 @@ export default { .map((label) => label.id); const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue }; - await this.setActiveIssueLabels(input); + await this.setActiveBoardItemLabels(input); } catch (e) { createFlash({ message: __('An error occurred while updating labels.') }); } finally { @@ -65,7 +65,7 @@ export default { try { const removeLabelIds = [getIdFromGraphQLId(id)]; const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue }; - await this.setActiveIssueLabels(input); + await this.setActiveBoardItemLabels(input); } catch (e) { createFlash({ message: __('An error occurred when removing the label.') }); } finally { diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue index cce3172d985..ad225c7bf5c 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue @@ -56,20 +56,20 @@ export default { }, }, computed: { - ...mapGetters(['activeIssue']), + ...mapGetters(['activeBoardItem']), hasMilestone() { - return this.activeIssue.milestone !== null; + return this.activeBoardItem.milestone !== null; }, groupFullPath() { - const { referencePath = '' } = this.activeIssue; + const { referencePath = '' } = this.activeBoardItem; return referencePath.slice(0, referencePath.indexOf('/')); }, projectPath() { - const { referencePath = '' } = this.activeIssue; + const { referencePath = '' } = this.activeBoardItem; return referencePath.slice(0, referencePath.indexOf('#')); }, dropdownText() { - return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone; + return this.activeBoardItem.milestone?.title ?? this.$options.i18n.noMilestone; }, }, methods: { @@ -118,7 +118,7 @@ export default { @close="handleClose" > <template v-if="hasMilestone" #collapsed> - <strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong> + <strong class="gl-text-gray-900">{{ activeBoardItem.milestone.title }}</strong> </template> <gl-dropdown ref="dropdown" @@ -131,7 +131,7 @@ export default { <gl-dropdown-item data-testid="no-milestone-item" :is-check-item="true" - :is-checked="!activeIssue.milestone" + :is-checked="!activeBoardItem.milestone" @click="setMilestone(null)" > {{ $options.i18n.noMilestone }} @@ -143,7 +143,7 @@ export default { v-for="milestone in milestones" :key="milestone.id" :is-check-item="true" - :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id" + :is-checked="activeBoardItem.milestone && milestone.id === activeBoardItem.milestone.id" data-testid="milestone-item" @click="setMilestone(milestone.id)" > diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue index f01c8e8fa20..8a7d0671fb3 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue @@ -27,9 +27,9 @@ export default { }; }, computed: { - ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), + ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']), notificationText() { - return this.activeIssue.emailsDisabled + return this.activeBoardItem.emailsDisabled ? this.$options.i18n.header.subscribeDisabledDescription : this.$options.i18n.header.title; }, @@ -41,7 +41,7 @@ export default { try { await this.setActiveIssueSubscribed({ - subscribed: !this.activeIssue.subscribed, + subscribed: !this.activeBoardItem.subscribed, projectPath: this.projectPathForActiveIssue, }); } catch (error) { @@ -61,8 +61,8 @@ export default { > <span data-testid="notification-header-text"> {{ notificationText }} </span> <gl-toggle - v-if="!activeIssue.emailsDisabled" - :value="activeIssue.subscribed" + v-if="!activeBoardItem.emailsDisabled" + :value="activeBoardItem.subscribed" :is-loading="loading" :label="$options.i18n.header.title" label-position="hidden" diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue index f853b744d23..96d444980a8 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue @@ -8,17 +8,17 @@ export default { }, inject: ['timeTrackingLimitToHours'], computed: { - ...mapGetters(['activeIssue']), + ...mapGetters(['activeBoardItem']), }, }; </script> <template> <issuable-time-tracker - :time-estimate="activeIssue.timeEstimate" - :time-spent="activeIssue.totalTimeSpent" - :human-time-estimate="activeIssue.humanTimeEstimate" - :human-time-spent="activeIssue.humanTotalTimeSpent" + :time-estimate="activeBoardItem.timeEstimate" + :time-spent="activeBoardItem.totalTimeSpent" + :human-time-estimate="activeBoardItem.humanTimeEstimate" + :human-time-spent="activeBoardItem.humanTotalTimeSpent" :limit-to-hours="timeTrackingLimitToHours" :show-collapsed="false" /> diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 4763902ce86..9211406d8b2 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -12,6 +12,7 @@ import { import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils'; +import { s__ } from '~/locale'; import { formatBoardLists, formatListIssues, @@ -371,20 +372,20 @@ export default { }, setAssignees: ({ commit, getters }, assigneeUsernames) => { - commit('UPDATE_ISSUE_BY_ID', { - issueId: getters.activeIssue.id, + commit('UPDATE_BOARD_ITEM_BY_ID', { + itemId: getters.activeBoardItem.id, prop: 'assignees', value: assigneeUsernames, }); }, setActiveIssueMilestone: async ({ commit, getters }, input) => { - const { activeIssue } = getters; + const { activeBoardItem } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetMilestoneMutation, variables: { input: { - iid: String(activeIssue.iid), + iid: String(activeBoardItem.iid), milestoneId: getIdFromGraphQLId(input.milestoneId), projectPath: input.projectPath, }, @@ -395,65 +396,71 @@ export default { throw new Error(data.updateIssue.errors); } - commit(types.UPDATE_ISSUE_BY_ID, { - issueId: activeIssue.id, + commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: activeBoardItem.id, prop: 'milestone', value: data.updateIssue.issue.milestone, }); }, - createNewIssue: ({ commit, state }, issueInput) => { - const { boardConfig } = state; + addListItem: ({ commit }, { list, item, position }) => { + commit(types.ADD_BOARD_ITEM_TO_LIST, { listId: list.id, itemId: item.id, atIndex: position }); + commit(types.UPDATE_BOARD_ITEM, item); + }, + + removeListItem: ({ commit }, { listId, itemId }) => { + commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { listId, itemId }); + commit(types.REMOVE_BOARD_ITEM, itemId); + }, + addListNewIssue: ( + { state: { boardConfig, boardType, fullPath }, dispatch, commit }, + { issueInput, list, placeholderId = `tmp-${new Date().getTime()}` }, + ) => { const input = formatIssueInput(issueInput, boardConfig); - const { boardType, fullPath } = state; if (boardType === BoardType.project) { input.projectPath = fullPath; } - return gqlClient + const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId }); + dispatch('addListItem', { list, item: placeholderIssue, position: 0 }); + + gqlClient .mutate({ mutation: issueCreateMutation, variables: { input }, }) .then(({ data }) => { if (data.createIssue.errors.length) { - commit(types.CREATE_ISSUE_FAILURE); - } else { - return data.createIssue?.issue; + throw new Error(); } - return null; - }) - .catch(() => commit(types.CREATE_ISSUE_FAILURE)); - }, - addListIssue: ({ commit }, { list, issue, position }) => { - commit(types.ADD_ISSUE_TO_LIST, { list, issue, position }); + const rawIssue = data.createIssue?.issue; + const formattedIssue = formatIssue({ ...rawIssue, id: getIdFromGraphQLId(rawIssue.id) }); + dispatch('removeListItem', { listId: list.id, itemId: placeholderId }); + dispatch('addListItem', { list, item: formattedIssue, position: 0 }); + }) + .catch(() => { + dispatch('removeListItem', { listId: list.id, itemId: placeholderId }); + commit( + types.SET_ERROR, + s__('Boards|An error occurred while creating the issue. Please try again.'), + ); + }); }, - addListNewIssue: ({ commit, dispatch }, { issueInput, list }) => { - const issue = formatIssue({ ...issueInput, id: 'tmp' }); - commit(types.ADD_ISSUE_TO_LIST, { list, issue, position: 0 }); - - dispatch('createNewIssue', issueInput) - .then((res) => { - commit(types.ADD_ISSUE_TO_LIST, { - list, - issue: formatIssue({ ...res, id: getIdFromGraphQLId(res.id) }), - }); - commit(types.REMOVE_ISSUE_FROM_LIST, { list, issue }); - }) - .catch(() => commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issueId: issueInput.id })); + setActiveBoardItemLabels: ({ dispatch }, params) => { + dispatch('setActiveIssueLabels', params); }, setActiveIssueLabels: async ({ commit, getters }, input) => { - const { activeIssue } = getters; + const { activeBoardItem } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetLabelsMutation, variables: { input: { - iid: String(activeIssue.iid), + iid: String(activeBoardItem.iid), addLabelIds: input.addLabelIds ?? [], removeLabelIds: input.removeLabelIds ?? [], projectPath: input.projectPath, @@ -465,20 +472,20 @@ export default { throw new Error(data.updateIssue.errors); } - commit(types.UPDATE_ISSUE_BY_ID, { - issueId: activeIssue.id, + commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: activeBoardItem.id, prop: 'labels', value: data.updateIssue.issue.labels.nodes, }); }, setActiveIssueDueDate: async ({ commit, getters }, input) => { - const { activeIssue } = getters; + const { activeBoardItem } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetDueDateMutation, variables: { input: { - iid: String(activeIssue.iid), + iid: String(activeBoardItem.iid), projectPath: input.projectPath, dueDate: input.dueDate, }, @@ -489,8 +496,8 @@ export default { throw new Error(data.updateIssue.errors); } - commit(types.UPDATE_ISSUE_BY_ID, { - issueId: activeIssue.id, + commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: activeBoardItem.id, prop: 'dueDate', value: data.updateIssue.issue.dueDate, }); @@ -501,7 +508,7 @@ export default { mutation: issueSetSubscriptionMutation, variables: { input: { - iid: String(getters.activeIssue.iid), + iid: String(getters.activeBoardItem.iid), projectPath: input.projectPath, subscribedState: input.subscribed, }, @@ -512,20 +519,20 @@ export default { throw new Error(data.issueSetSubscription.errors); } - commit(types.UPDATE_ISSUE_BY_ID, { - issueId: getters.activeIssue.id, + commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: getters.activeBoardItem.id, prop: 'subscribed', value: data.issueSetSubscription.issue.subscribed, }); }, setActiveIssueTitle: async ({ commit, getters }, input) => { - const { activeIssue } = getters; + const { activeBoardItem } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetTitleMutation, variables: { input: { - iid: String(activeIssue.iid), + iid: String(activeBoardItem.iid), projectPath: input.projectPath, title: input.title, }, @@ -536,8 +543,8 @@ export default { throw new Error(data.updateIssue.errors); } - commit(types.UPDATE_ISSUE_BY_ID, { - issueId: activeIssue.id, + commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: activeBoardItem.id, prop: 'title', value: data.updateIssue.issue.title, }); @@ -578,10 +585,10 @@ export default { const { selectedBoardItems } = state; const index = selectedBoardItems.indexOf(boardItem); - // If user already selected an item (activeIssue) without using mult-select, + // If user already selected an item (activeBoardItem) without using mult-select, // include that item in the selection and unset state.ActiveId to hide the sidebar. - if (getters.activeIssue) { - commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeIssue); + if (getters.activeBoardItem) { + commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeBoardItem); dispatch('unsetActiveId'); } diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index c819717dade..0589851c658 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -15,17 +15,17 @@ export default { return listItemsIds.map((id) => getters.getBoardItemById(id)); }, - activeIssue: (state) => { + activeBoardItem: (state) => { return state.boardItems[state.activeId] || {}; }, groupPathForActiveIssue: (_, getters) => { - const { referencePath = '' } = getters.activeIssue; + const { referencePath = '' } = getters.activeBoardItem; return referencePath.slice(0, referencePath.indexOf('/')); }, projectPathForActiveIssue: (_, getters) => { - const { referencePath = '' } = getters.activeIssue; + const { referencePath = '' } = getters.activeBoardItem; return referencePath.slice(0, referencePath.indexOf('#')); }, diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index e1f00e4f051..7ff1f8296ca 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -20,23 +20,23 @@ export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE'; export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST'; export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE'; export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS'; -export const CREATE_ISSUE_FAILURE = 'CREATE_ISSUE_FAILURE'; export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS'; export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR'; export const MOVE_ISSUE = 'MOVE_ISSUE'; export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS'; export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE'; +export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM'; +export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM'; export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; -export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST'; -export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE'; -export const REMOVE_ISSUE_FROM_LIST = 'REMOVE_ISSUE_FROM_LIST'; +export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST'; +export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST'; export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; -export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID'; +export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID'; export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; export const RESET_ISSUES = 'RESET_ISSUES'; export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 802131d4671..495b3b31df5 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -158,13 +158,13 @@ export default { }); }, - [mutationTypes.UPDATE_ISSUE_BY_ID]: (state, { issueId, prop, value }) => { - if (!state.boardItems[issueId]) { + [mutationTypes.UPDATE_BOARD_ITEM_BY_ID]: (state, { itemId, prop, value }) => { + if (!state.boardItems[itemId]) { /* eslint-disable-next-line @gitlab/require-i18n-strings */ throw new Error('No issue found.'); } - Vue.set(state.boardItems[issueId], prop, value); + Vue.set(state.boardItems[itemId], prop, value); }, [mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) { @@ -229,28 +229,23 @@ export default { notImplemented(); }, - [mutationTypes.CREATE_ISSUE_FAILURE]: (state) => { - state.error = s__('Boards|An error occurred while creating the issue. Please try again.'); + [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: ( + state, + { itemId, listId, moveBeforeId, moveAfterId, atIndex }, + ) => { + addItemToList({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex }); }, - [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => { - addItemToList({ - state, - listId: list.id, - itemId: issue.id, - atIndex: position, - }); - Vue.set(state.boardItems, issue.id, issue); + [mutationTypes.REMOVE_BOARD_ITEM_FROM_LIST]: (state, { itemId, listId }) => { + removeItemFromList({ state, listId, itemId }); }, - [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issueId }) => { - state.error = s__('Boards|An error occurred while creating the issue. Please try again.'); - removeItemFromList({ state, listId: list.id, itemId: issueId }); + [mutationTypes.UPDATE_BOARD_ITEM]: (state, item) => { + Vue.set(state.boardItems, item.id, item); }, - [mutationTypes.REMOVE_ISSUE_FROM_LIST]: (state, { list, issue }) => { - removeItemFromList({ state, listId: list.id, itemId: issue.id }); - Vue.delete(state.boardItems, issue.id); + [mutationTypes.REMOVE_BOARD_ITEM]: (state, itemId) => { + Vue.delete(state.boardItems, itemId); }, [mutationTypes.SET_CURRENT_PAGE]: () => { diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss index 568d71105fa..e8e3a8f6591 100644 --- a/app/assets/stylesheets/page_bundles/boards.scss +++ b/app/assets/stylesheets/page_bundles/boards.scss @@ -365,7 +365,7 @@ margin: 5px; } -.right-sidebar.issue-boards-sidebar { +.right-sidebar.boards-sidebar { .gutter-toggle { bottom: 15px; width: 22px; @@ -462,7 +462,7 @@ overflow-x: scroll; } - .issue-boards-sidebar { + .boards-sidebar { height: 100%; top: 0; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0a36051948e..606a70abbb5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -287,7 +287,7 @@ padding-top: 10px; } - &:not(.issue-boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) { + &:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) { .issuable-sidebar-header { display: none; } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fbff8ade761..77e5ec209e3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -286,9 +286,11 @@ module Ci end after_transition any => [:failed] do |pipeline| - next unless pipeline.auto_devops_source? + pipeline.run_after_commit do + ::Gitlab::Ci::Pipeline::Metrics.pipeline_failure_reason_counter.increment(reason: pipeline.failure_reason) - pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) } + AutoDevops::DisableWorker.perform_async(pipeline.id) if pipeline.auto_devops_source? + end end end @@ -584,10 +586,18 @@ module Ci end def cancel_running(retries: nil) - retry_optimistic_lock(cancelable_statuses, retries, name: 'ci_pipeline_cancel_running') do |cancelable| - cancelable.find_each do |job| - yield(job) if block_given? - job.cancel + commit_status_relations = [:project, :pipeline] + ci_build_relations = [:deployment, :taggings] + + retry_optimistic_lock(cancelable_statuses, retries, name: 'ci_pipeline_cancel_running') do |cancelables| + cancelables.find_in_batches do |batch| + ActiveRecord::Associations::Preloader.new.preload(batch, commit_status_relations) + ActiveRecord::Associations::Preloader.new.preload(batch.select { |job| job.is_a?(Ci::Build) }, ci_build_relations) + + batch.each do |job| + yield(job) if block_given? + job.cancel + end end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 292d249cb23..ce253d5ad81 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -181,15 +181,16 @@ class CommitStatus < ApplicationRecord end after_transition any => :failed do |commit_status| - next if Feature.enabled?(:async_add_build_failure_todo, commit_status.project, default_enabled: :yaml) - next unless commit_status.project - - # rubocop: disable CodeReuse/ServiceClass commit_status.run_after_commit do - MergeRequests::AddTodoWhenBuildFailsService - .new(project, nil).execute(self) + ::Gitlab::Ci::Pipeline::Metrics.job_failure_reason_counter.increment(reason: commit_status.failure_reason) + + # rubocop: disable CodeReuse/ServiceClass + next if Feature.enabled?(:async_add_build_failure_todo, commit_status.project, default_enabled: :yaml) + next unless commit_status.project + + MergeRequests::AddTodoWhenBuildFailsService.new(project, nil).execute(self) + # rubocop: enable CodeReuse/ServiceClass end - # rubocop: enable CodeReuse/ServiceClass end end diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb index 15458caa9f3..f522e98b448 100644 --- a/app/serializers/build_artifact_entity.rb +++ b/app/serializers/build_artifact_entity.rb @@ -15,18 +15,18 @@ class BuildArtifactEntity < Grape::Entity expose :path do |artifact| fast_download_project_job_artifacts_path( - artifact.project, + project, artifact.job, file_type: artifact.file_type ) end - expose :keep_path, if: -> (*) { artifact.expiring? && show_duplicated_paths?(artifact.project) } do |artifact| - fast_keep_project_job_artifacts_path(artifact.project, artifact.job) + expose :keep_path, if: -> (*) { artifact.expiring? && show_duplicated_paths?(project) } do |artifact| + fast_keep_project_job_artifacts_path(project, artifact.job) end - expose :browse_path, if: -> (*) { show_duplicated_paths?(artifact.project) } do |artifact| - fast_browse_project_job_artifacts_path(artifact.project, artifact.job) + expose :browse_path, if: -> (*) { show_duplicated_paths?(project) } do |artifact| + fast_browse_project_job_artifacts_path(project, artifact.job) end private @@ -34,4 +34,8 @@ class BuildArtifactEntity < Grape::Entity def show_duplicated_paths?(project) !Gitlab::Ci::Features.remove_duplicate_artifact_exposure_paths?(project) end + + def project + options[:project] || artifact.project + end end diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb index c7caad0e62b..21180ab82ab 100644 --- a/app/serializers/merge_requests/pipeline_entity.rb +++ b/app/serializers/merge_requests/pipeline_entity.rb @@ -28,7 +28,7 @@ class MergeRequests::PipelineEntity < Grape::Entity rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) } end - BuildArtifactEntity.represent(rel, options) + BuildArtifactEntity.represent(rel, options.merge(project: pipeline.project)) end expose :detailed_status, as: :status, with: DetailedStatusEntity do |pipeline| diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 4fec543eca8..bb6aa2f78ac 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -15,7 +15,7 @@ class PipelineDetailsEntity < Ci::PipelineEntity rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) } end - BuildArtifactEntity.represent(rel, options) + BuildArtifactEntity.represent(rel, options.merge(project: pipeline.project)) end expose :manual_actions, using: BuildActionEntity expose :scheduled_actions, using: BuildActionEntity diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 970652b4da3..6c69df0c616 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -19,7 +19,7 @@ module Ci end def metrics - @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new + @metrics ||= ::Gitlab::Ci::Pipeline::Metrics end private diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index da00879ecf9..106a7832cc7 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -41,25 +41,25 @@ = link_to '#tab-members', class: ['nav-link', ('active' unless invited_active)], data: { toggle: 'tab' } do %span = _('Members') - %span.badge.badge-pill= @members.total_count + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @members.total_count - if @group.shared_with_group_links.any? %li.nav-item = link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do %span = _('Groups') - %span.badge.badge-pill= @group.shared_with_group_links.count + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @group.shared_with_group_links.count - if show_invited_members %li.nav-item = link_to '#tab-invited-members', class: ['nav-link', ('active' if invited_active)], data: { toggle: 'tab' } do %span = _('Invited') - %span.badge.badge-pill= @invited_members.total_count + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @invited_members.total_count - if show_access_requests %li.nav-item = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do %span = _('Access requests') - %span.badge.badge-pill= @requesters.count + %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count .tab-content #tab-members.tab-pane{ class: ('active' unless invited_active) } .js-group-members-list{ data: group_members_list_data_attributes(@group, @members) } diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml index 59dd571604b..8976e89b3d3 100644 --- a/app/views/shared/boards/components/_sidebar.html.haml +++ b/app/views/shared/boards/components/_sidebar.html.haml @@ -1,6 +1,6 @@ %board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json } %transition{ name: "boards-sidebar-slide" } - %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar", 'aria-label': s_('Boards|Board') } + %aside.right-sidebar.right-sidebar-expanded.boards-sidebar{ "v-show" => "showSidebar", 'aria-label': s_('Boards|Board'), 'data-testid': 'issue-boards-sidebar' } .issuable-sidebar .block.issuable-sidebar-header.position-relative %span.issuable-header-text.hide-collapsed.float-left diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index ef1092b83fa..7466f360f67 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -24,7 +24,7 @@ .avatar-container.s48.flex-grow-0.flex-shrink-0{ class: avatar_container_class } = link_to project_path(project), class: dom_class(project) do - if project.creator && use_creator_avatar - = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s48", alt:'' + = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s48", alt: '' - else = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48) .project-details.d-sm-flex.flex-sm-fill.align-items-center{ data: { qa_selector: 'project_content', qa_project_name: project.name } } diff --git a/changelogs/unreleased/21098-reducing-cancel-sql-queries.yml b/changelogs/unreleased/21098-reducing-cancel-sql-queries.yml new file mode 100644 index 00000000000..61006357e36 --- /dev/null +++ b/changelogs/unreleased/21098-reducing-cancel-sql-queries.yml @@ -0,0 +1,5 @@ +--- +title: Preload associations in Ci::Pipeline#cancel_running +merge_request: 58484 +author: +type: performance diff --git a/changelogs/unreleased/board-view-deprecated-button.yml b/changelogs/unreleased/board-view-deprecated-button.yml new file mode 100644 index 00000000000..cecba250aac --- /dev/null +++ b/changelogs/unreleased/board-view-deprecated-button.yml @@ -0,0 +1,5 @@ +--- +title: Replace deprecated buttons on board view +merge_request: 58153 +author: +type: changed diff --git a/changelogs/unreleased/btn-confirm-project-settings.yml b/changelogs/unreleased/btn-confirm-project-settings.yml new file mode 100644 index 00000000000..c92b004c918 --- /dev/null +++ b/changelogs/unreleased/btn-confirm-project-settings.yml @@ -0,0 +1,5 @@ +--- +title: Move to btn-confirm from btn-success in ee project settings +merge_request: 58047 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/gl-badge-members.yml b/changelogs/unreleased/gl-badge-members.yml new file mode 100644 index 00000000000..0a309e7d7ca --- /dev/null +++ b/changelogs/unreleased/gl-badge-members.yml @@ -0,0 +1,5 @@ +--- +title: Add gl-badge for badges in group members page +merge_request: 57933 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/id-reduce-cached-requests.yml b/changelogs/unreleased/id-reduce-cached-requests.yml new file mode 100644 index 00000000000..9a08a239c9b --- /dev/null +++ b/changelogs/unreleased/id-reduce-cached-requests.yml @@ -0,0 +1,5 @@ +--- +title: Reduce SQL requests on building artifacts +merge_request: 58339 +author: +type: performance diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-groups-module.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-groups-module.yml new file mode 100644 index 00000000000..44df3c067dc --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-groups-module.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe Rubocop offenses for groups module +merge_request: 58183 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-projects-controller.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-projects-controller.yml new file mode 100644 index 00000000000..d56d321b93c --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-projects-controller.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe Rubocop offenses for projects controller +merge_request: 58176 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: other diff --git a/changelogs/unreleased/mwaw-301055-update-metrics-definitions-for-product-intelligence-group.yml b/changelogs/unreleased/mwaw-301055-update-metrics-definitions-for-product-intelligence-group.yml new file mode 100644 index 00000000000..1e0527a4150 --- /dev/null +++ b/changelogs/unreleased/mwaw-301055-update-metrics-definitions-for-product-intelligence-group.yml @@ -0,0 +1,5 @@ +--- +title: Deprecate Product Intelligence test aggregated metrics +merge_request: 57377 +author: +type: deprecated diff --git a/changelogs/unreleased/use-atomic-mutations-boards.yml b/changelogs/unreleased/use-atomic-mutations-boards.yml new file mode 100644 index 00000000000..d9916dceaf6 --- /dev/null +++ b/changelogs/unreleased/use-atomic-mutations-boards.yml @@ -0,0 +1,5 @@ +--- +title: If creating a new issue fails in boards, remove the issue card from a list +merge_request: 58558 +author: +type: other diff --git a/config/metrics/aggregates/common.yml b/config/metrics/aggregates/common.yml index 15fc6915977..beabb72dd72 100644 --- a/config/metrics/aggregates/common.yml +++ b/config/metrics/aggregates/common.yml @@ -27,22 +27,6 @@ - 'i_compliance_audit_events' - 'a_compliance_audit_events_api' - 'i_compliance_credential_inventory' -- name: product_analytics_test_metrics_union - operator: OR - source: redis - time_frame: [7d, 28d] - events: - - 'i_search_total' - - 'i_search_advanced' - - 'i_search_paid' -- name: product_analytics_test_metrics_intersection - operator: AND - source: redis - time_frame: [7d, 28d] - events: - - 'i_search_total' - - 'i_search_advanced' - - 'i_search_paid' - name: incident_management_alerts_total_unique_counts operator: OR source: redis diff --git a/config/metrics/counts_28d/20210216183203_product_analytics_test_metrics_union.yml b/config/metrics/counts_28d/20210216183203_product_analytics_test_metrics_union.yml index f4723c2b5a1..1f9832b220d 100644 --- a/config/metrics/counts_28d/20210216183203_product_analytics_test_metrics_union.yml +++ b/config/metrics/counts_28d/20210216183203_product_analytics_test_metrics_union.yml @@ -1,16 +1,21 @@ --- key_path: counts_monthly.aggregated_metrics.product_analytics_test_metrics_union -description: '' -product_section: '' -product_stage: '' -product_group: '' -product_category: '' +description: This was test metric used for purpose of assuring correct implementation of aggregated metrics feature +product_section: growth +product_stage: growth +product_group: group::product intelligence +product_category: collection value_type: number -status: data_available -time_frame: 28d -data_source: database +status: removed +milestone_removed: '13.11' +milestone: '13.7' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49886 +time_frame: 7d +data_source: redis_hll distribution: -- ce + - ce + - ee tier: -- free -skip_validation: true + - free + - premium + - ultimate diff --git a/config/metrics/counts_28d/20210216183205_product_analytics_test_metrics_intersection.yml b/config/metrics/counts_28d/20210216183205_product_analytics_test_metrics_intersection.yml index fe8073f3f33..171b2a0a032 100644 --- a/config/metrics/counts_28d/20210216183205_product_analytics_test_metrics_intersection.yml +++ b/config/metrics/counts_28d/20210216183205_product_analytics_test_metrics_intersection.yml @@ -1,16 +1,21 @@ --- key_path: counts_monthly.aggregated_metrics.product_analytics_test_metrics_intersection -description: '' -product_section: '' -product_stage: '' -product_group: '' -product_category: '' +description: This was test metric used for purpose of assuring correct implementation of aggregated metrics feature +product_section: growth +product_stage: growth +product_group: group::product intelligence +product_category: collection value_type: number -status: data_available +status: removed +milestone_removed: '13.11' +milestone: '13.7' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49886 time_frame: 28d -data_source: database +data_source: redis_hll distribution: -- ce + - ce + - ee tier: -- free -skip_validation: true + - free + - premium + - ultimate diff --git a/config/metrics/counts_7d/20210216183213_product_analytics_test_metrics_union.yml b/config/metrics/counts_7d/20210216183213_product_analytics_test_metrics_union.yml new file mode 100644 index 00000000000..7443c3d599f --- /dev/null +++ b/config/metrics/counts_7d/20210216183213_product_analytics_test_metrics_union.yml @@ -0,0 +1,21 @@ +--- +key_path: counts_weekly.aggregated_metrics.product_analytics_test_metrics_union +description: This was test metric used for purpose of assuring correct implementation of aggregated metrics feature +product_section: growth +product_stage: growth +product_group: group::product intelligence +product_category: collection +value_type: number +status: removed +milestone_removed: '13.11' +milestone: '13.7' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49886 +time_frame: 7d +data_source: redis_hll +distribution: + - ce + - ee +tier: + - free + - premium + - ultimate diff --git a/config/metrics/counts_7d/20210216183215_product_analytics_test_metrics_intersection.yml b/config/metrics/counts_7d/20210216183215_product_analytics_test_metrics_intersection.yml new file mode 100644 index 00000000000..d4d6a713936 --- /dev/null +++ b/config/metrics/counts_7d/20210216183215_product_analytics_test_metrics_intersection.yml @@ -0,0 +1,21 @@ +--- +key_path: counts_weekly.aggregated_metrics.product_analytics_test_metrics_intersection +description: This was test metric used for purpose of assuring correct implementation of aggregated metrics feature +product_section: growth +product_stage: growth +product_group: group::product intelligence +product_category: collection +value_type: number +status: removed +milestone_removed: '13.11' +milestone: '13.7' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49886 +time_frame: 7d +data_source: redis_hll +distribution: + - ce + - ee +tier: + - free + - premium + - ultimate diff --git a/config/metrics/schema.json b/config/metrics/schema.json index 8878b3bc744..4c10aca7061 100644 --- a/config/metrics/schema.json +++ b/config/metrics/schema.json @@ -30,7 +30,7 @@ }, "status": { "type": ["string"], - "enum": ["data_available", "implemented", "not_used", "deprecated"] + "enum": ["data_available", "implemented", "not_used", "deprecated", "removed"] }, "milestone": { "type": ["string", "null"], diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 7fd457cd07d..6b336e60be1 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -601,3 +601,22 @@ gitlab_rails['artifacts_object_store_direct_upload'] = true To prevent this, comment out or remove those lines, or switch to their [default values](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template), then run `sudo gitlab-ctl reconfigure`. + +### Job artifact upload fails with error 500 + +If you are using object storage for artifacts and a job artifact fails to upload, +you can check: + +- The job log for an error similar to: + + ```plaintext + WARNING: Uploading artifacts as "archive" to coordinator... failed id=12345 responseStatus=500 Internal Server Error status=500 token=abcd1234 + ``` + +- The [workhorse log](logs.md#workhorse-logs) for an error similar to: + + ```json + {"error":"MissingRegion: could not find region configuration","level":"error","msg":"error uploading S3 session","time":"2021-03-16T22:10:55-04:00"} + ``` + +In both cases, you might need to add `region` to the job artifact [object storage configuration](#connection-settings). diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 24cd5e10f89..7a0cca9dccb 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -6154,27 +6154,27 @@ Tiers: `free` ### `counts_monthly.aggregated_metrics.product_analytics_test_metrics_intersection` -Missing description +This was test metric used for purpose of assuring correct implementation of aggregated metrics feature [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216183205_product_analytics_test_metrics_intersection.yml) -Group: `` +Group: `group::product intelligence` -Status: `data_available` +Status: `removed` -Tiers: `free` +Tiers: `free`, `premium`, `ultimate` ### `counts_monthly.aggregated_metrics.product_analytics_test_metrics_union` -Missing description +This was test metric used for purpose of assuring correct implementation of aggregated metrics feature [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216183203_product_analytics_test_metrics_union.yml) -Group: `` +Group: `group::product intelligence` -Status: `data_available` +Status: `removed` -Tiers: `free` +Tiers: `free`, `premium`, `ultimate` ### `counts_monthly.deployments` @@ -6322,27 +6322,27 @@ Tiers: ### `counts_weekly.aggregated_metrics.product_analytics_test_metrics_intersection` -Missing description +This was test metric used for purpose of assuring correct implementation of aggregated metrics feature -[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216183215_product_analytics_test_metrics_intersection.yml) +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210216183215_product_analytics_test_metrics_intersection.yml) -Group: `` +Group: `group::product intelligence` -Status: `data_available` +Status: `removed` -Tiers: +Tiers: `free`, `premium`, `ultimate` ### `counts_weekly.aggregated_metrics.product_analytics_test_metrics_union` -Missing description +This was test metric used for purpose of assuring correct implementation of aggregated metrics feature -[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216183213_product_analytics_test_metrics_union.yml) +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210216183213_product_analytics_test_metrics_union.yml) -Group: `` +Group: `group::product intelligence` -Status: `data_available` +Status: `removed` -Tiers: +Tiers: `free`, `premium`, `ultimate` ### `database.adapter` diff --git a/doc/development/usage_ping/metrics_dictionary.md b/doc/development/usage_ping/metrics_dictionary.md index 047fb04768c..261c5b8078f 100644 --- a/doc/development/usage_ping/metrics_dictionary.md +++ b/doc/development/usage_ping/metrics_dictionary.md @@ -33,7 +33,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields: | `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the metric. | | `product_category` | no | The [product category](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/categories.yml) for the metric. | | `value_type` | yes | `string`; one of [`string`, `number`, `boolean`, `object`](https://json-schema.org/understanding-json-schema/reference/type.html). | -| `status` | yes | `string`; [status](#metric-statuses) of the metric, may be set to `data_available`, `implemented`, `not_used`, `deprecated`. | +| `status` | yes | `string`; [status](#metric-statuses) of the metric, may be set to `data_available`, `implemented`, `not_used`, `deprecated`, `removed`. | | `time_frame` | yes | `string`; may be set to a value like `7d`, `28d`, `all`, `none`. | | `data_source` | yes | `string`; may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `ruby`. | | `distribution` | yes | `array`; may be set to one of `ce, ee` or `ee`. The [distribution](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the tracked feature is available. | @@ -52,6 +52,7 @@ Metric definitions can have one of the following statuses: status for newly added metrics awaiting inclusion in a new release. - `not_used`: Metric is not used in any dashboard. - `deprecated`: Metric is deprecated and possibly planned to be removed. +- `removed`: Metric was removed, but it may appear in Usage Ping payloads sent from instances running on older versions of GitLab. ### Example YAML metric definition diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 46ecb10ea2b..c3c1728602c 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -84,7 +84,7 @@ module Gitlab end def metrics - @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new + @metrics ||= ::Gitlab::Ci::Pipeline::Metrics end def observe_creation_duration(duration) @@ -97,6 +97,11 @@ module Gitlab .observe({ source: pipeline.source.to_s }, pipeline.total_size) end + def increment_pipeline_failure_reason_counter(reason) + metrics.pipeline_failure_reason_counter + .increment(reason: (reason || :unknown_failure).to_s) + end + def dangling_build? %i[ondemand_dast_scan webide].include?(source) end diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb index d7271df1694..c59a2194823 100644 --- a/lib/gitlab/ci/pipeline/chain/helpers.rb +++ b/lib/gitlab/ci/pipeline/chain/helpers.rb @@ -12,7 +12,8 @@ module Gitlab end pipeline.add_error_message(message) - pipeline.drop!(drop_reason) if drop_reason && persist_pipeline? + + drop_pipeline!(drop_reason) # TODO: consider not to rely on AR errors directly as they can be # polluted with other unrelated errors (e.g. state machine) @@ -24,8 +25,16 @@ module Gitlab pipeline.add_warning_message(message) end - def persist_pipeline? - command.save_incompleted && !pipeline.readonly? + private + + def drop_pipeline!(drop_reason) + return if pipeline.readonly? + + if drop_reason && command.save_incompleted + pipeline.drop!(drop_reason) + else + command.increment_pipeline_failure_reason_counter(drop_reason) + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/metrics.rb b/lib/gitlab/ci/pipeline/chain/metrics.rb index 0d7449813b4..b17ae77d445 100644 --- a/lib/gitlab/ci/pipeline/chain/metrics.rb +++ b/lib/gitlab/ci/pipeline/chain/metrics.rb @@ -14,7 +14,7 @@ module Gitlab end def counter - ::Gitlab::Ci::Pipeline::Metrics.new.pipelines_created_counter + ::Gitlab::Ci::Pipeline::Metrics.pipelines_created_counter end end end diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb index c77f4dcca5a..50cc08c0d66 100644 --- a/lib/gitlab/ci/pipeline/metrics.rb +++ b/lib/gitlab/ci/pipeline/metrics.rb @@ -4,9 +4,9 @@ module Gitlab module Ci module Pipeline class Metrics - include Gitlab::Utils::StrongMemoize + extend Gitlab::Utils::StrongMemoize - def pipeline_creation_duration_histogram + def self.pipeline_creation_duration_histogram strong_memoize(:pipeline_creation_duration_histogram) do name = :gitlab_ci_pipeline_creation_duration_seconds comment = 'Pipeline creation duration' @@ -17,7 +17,7 @@ module Gitlab end end - def pipeline_size_histogram + def self.pipeline_size_histogram strong_memoize(:pipeline_size_histogram) do name = :gitlab_ci_pipeline_size_builds comment = 'Pipeline size' @@ -28,7 +28,7 @@ module Gitlab end end - def pipeline_processing_events_counter + def self.pipeline_processing_events_counter strong_memoize(:pipeline_processing_events_counter) do name = :gitlab_ci_pipeline_processing_events_total comment = 'Total amount of pipeline processing events' @@ -37,7 +37,7 @@ module Gitlab end end - def pipelines_created_counter + def self.pipelines_created_counter strong_memoize(:pipelines_created_count) do name = :pipelines_created_total comment = 'Counter of pipelines created' @@ -46,7 +46,7 @@ module Gitlab end end - def legacy_update_jobs_counter + def self.legacy_update_jobs_counter strong_memoize(:legacy_update_jobs_counter) do name = :ci_legacy_update_jobs_as_retried_total comment = 'Counter of occurrences when jobs were not being set as retried before update_retried' @@ -54,6 +54,24 @@ module Gitlab Gitlab::Metrics.counter(name, comment) end end + + def self.pipeline_failure_reason_counter + strong_memoize(:pipeline_failure_reason_counter) do + name = :gitlab_ci_pipeline_failure_reasons + comment = 'Counter of pipeline failure reasons' + + Gitlab::Metrics.counter(name, comment) + end + end + + def self.job_failure_reason_counter + strong_memoize(:job_failure_reason_counter) do + name = :gitlab_ci_job_failure_reasons + comment = 'Counter of job failure reasons' + + Gitlab::Metrics.counter(name, comment) + end + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0ace5a67816..75054e1b9af 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12108,6 +12108,9 @@ msgstr "" msgid "Epic cannot be found." msgstr "" +msgid "Epic details" +msgstr "" + msgid "Epic events" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb index 2ddc59acd5c..7c71228c767 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb @@ -69,14 +69,15 @@ module QA Page::MergeRequest::Show.perform do |mr| mr.merge_when_pipeline_succeeds! - expect(mr.merge_request_status).to match(/to be merged automatically when the pipeline succeeds/) - Support::Waiter.wait_until(sleep_interval: 5) do merge_request = merge_request.reload! merge_request.state == 'merged' end - expect(mr.merged?).to be_truthy, "Expected content 'The changes were merged' but it did not appear." + aggregate_failures do + expect(merge_request.merge_when_pipeline_succeeds).to be_truthy + expect(mr.merged?).to be_truthy, "Expected content 'The changes were merged' but it did not appear." + end end end end diff --git a/spec/controllers/projects/alerting/notifications_controller_spec.rb b/spec/controllers/projects/alerting/notifications_controller_spec.rb index 3656cfbcc30..fe0c4ce00bf 100644 --- a/spec/controllers/projects/alerting/notifications_controller_spec.rb +++ b/spec/controllers/projects/alerting/notifications_controller_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Projects::Alerting::NotificationsController do let_it_be(:project) { create(:project) } let_it_be(:environment) { create(:environment, project: project) } + let(:params) { project_params } describe 'POST #create' do @@ -68,6 +69,7 @@ RSpec.describe Projects::Alerting::NotificationsController do context 'with a corresponding integration' do context 'with integration parameters specified' do let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) } + let(:params) { project_params(endpoint_identifier: integration.endpoint_identifier, name: integration.name) } context 'the integration is active' do diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb index e0f86876f67..d2e0def6d0f 100644 --- a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb +++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Projects::DesignManagement::Designs::RawImagesController do let_it_be(:project) { create(:project, :private) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:viewer) { issue.author } + let(:design_id) { design.id } let(:sha) { design.versions.first.sha } let(:filename) { design.filename } diff --git a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb index 96ecbaf55b6..56c0ef592ca 100644 --- a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb +++ b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Projects::DesignManagement::Designs::ResizedImageController do let_it_be(:issue) { create(:issue, project: project) } let_it_be(:viewer) { issue.author } let_it_be(:size) { :v432x230 } + let(:design) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 2) } let(:design_id) { design.id } let(:sha) { design.versions.first.sha } diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 83ad36b217f..4cb90edb742 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Projects::EnvironmentsController do let_it_be(:project) { create(:project) } let_it_be(:maintainer) { create(:user, name: 'main-dos').tap { |u| project.add_maintainer(u) } } let_it_be(:reporter) { create(:user, name: 'repo-dos').tap { |u| project.add_reporter(u) } } + let(:user) { maintainer } let!(:environment) { create(:environment, name: 'production', project: project) } diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb index f69cc0ddfd8..cd7d1ea0e8a 100644 --- a/spec/controllers/projects/feature_flags_controller_spec.rb +++ b/spec/controllers/projects/feature_flags_controller_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Projects::FeatureFlagsController do let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } + let(:user) { developer } before_all do diff --git a/spec/controllers/projects/incidents_controller_spec.rb b/spec/controllers/projects/incidents_controller_spec.rb index ddd15b9b1dd..460821634b0 100644 --- a/spec/controllers/projects/incidents_controller_spec.rb +++ b/spec/controllers/projects/incidents_controller_spec.rb @@ -69,6 +69,7 @@ RSpec.describe Projects::IncidentsController do end let_it_be(:resource) { create(:incident, project: project) } + let(:user) { developer } it 'renders incident page' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 992b76ed24a..5be98dbb096 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Projects::IssuesController do let_it_be(:project, reload: true) { create(:project) } let_it_be(:user, reload: true) { create(:user) } + let(:issue) { create(:issue, project: project) } let(:spam_action_response_fields) { { 'stub_spam_action_response_fields' => true } } @@ -369,6 +370,7 @@ RSpec.describe Projects::IssuesController do end let_it_be(:issue) { create(:issue, project: project) } + let(:developer) { user } let(:params) do { @@ -1185,6 +1187,7 @@ RSpec.describe Projects::IssuesController do context 'resolving discussions in MergeRequest' do let_it_be(:discussion) { create(:diff_note_on_merge_request).to_discussion } + let(:merge_request) { discussion.noteable } let(:project) { merge_request.source_project } @@ -1648,6 +1651,7 @@ RSpec.describe Projects::IssuesController do describe 'POST #import_csv' do let_it_be(:project) { create(:project, :public) } + let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') } context 'unauthorized' do @@ -1847,6 +1851,7 @@ RSpec.describe Projects::IssuesController do context 'with cross-reference system note', :request_store do let_it_be(:new_issue) { create(:issue) } + let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" } before do diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 80e1268cb01..a7a36d3a074 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -1275,6 +1275,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do let_it_be(:reporter) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:project) { create(:project, :private, :repository, namespace: owner.namespace) } + let(:user) { maintainer } let(:pipeline) { create(:ci_pipeline, project: project, source: :webide, config_source: :webide_source, user: user) } let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline, user: user) } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d452f69d6fb..6644373f758 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Projects::MergeRequestsController do let_it_be_with_refind(:project) { create(:project, :repository) } let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) } + let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } diff --git a/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb b/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb index 8a344a72120..923581d9367 100644 --- a/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb +++ b/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Projects::PerformanceMonitoring::DashboardsController do let_it_be(:user) { create(:user) } let_it_be(:namespace) { create(:namespace) } + let!(:project) { create(:project, :repository, name: 'dashboard-project', namespace: namespace) } let(:repository) { project.repository } let(:branch) { double(name: branch_name) } diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index abc24163e7b..16bce104630 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Projects::PipelinesController do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, :repository) } + let(:feature) { ProjectFeature::ENABLED } before do diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb index ad631b7c3da..39fb153e802 100644 --- a/spec/controllers/projects/pipelines_settings_controller_spec.rb +++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Projects::PipelinesSettingsController do let_it_be(:user) { create(:user) } let_it_be(:project_auto_devops) { create(:project_auto_devops) } + let(:project) { project_auto_devops.project } before do diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index b1c3c1c0276..5dee36ee7c2 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Projects::RawController do include RepoHelpers let_it_be(:project) { create(:project, :public, :repository) } + let(:inline) { nil } describe 'GET #show' do diff --git a/spec/controllers/projects/releases/evidences_controller_spec.rb b/spec/controllers/projects/releases/evidences_controller_spec.rb index 0a83cdb19fe..68433969d69 100644 --- a/spec/controllers/projects/releases/evidences_controller_spec.rb +++ b/spec/controllers/projects/releases/evidences_controller_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Projects::Releases::EvidencesController do let_it_be(:private_project) { create(:project, :repository, :private) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } + let(:user) { developer } before do diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index fc7ab88bbe0..a1e36ec5c4c 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Projects::ReleasesController do let_it_be(:reporter) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:user) { developer } + let!(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) } let!(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) } diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index 7a6e11d53d4..d953249c139 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -5,6 +5,7 @@ require('spec_helper') RSpec.describe Projects::Settings::CiCdController do let_it_be(:user) { create(:user) } let_it_be(:project_auto_devops) { create(:project_auto_devops) } + let(:project) { project_auto_devops.project } before do diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb index 46f69eaf96a..d2934ec4e97 100644 --- a/spec/controllers/projects/settings/operations_controller_spec.rb +++ b/spec/controllers/projects/settings/operations_controller_spec.rb @@ -493,6 +493,7 @@ RSpec.describe Projects::Settings::OperationsController do describe 'PATCH #update' do let_it_be(:external_url) { 'https://gitlab.com' } + let(:params) do { tracing_setting_attributes: { diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb index b563f3b667f..73b0e3bba69 100644 --- a/spec/controllers/projects/static_site_editor_controller_spec.rb +++ b/spec/controllers/projects/static_site_editor_controller_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Projects::StaticSiteEditorController do let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:user) { create(:user) } + let(:data) { { key: 'value' } } describe 'GET index' do diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb index 0e35f401bc8..9a73417ffdb 100644 --- a/spec/controllers/projects/todos_controller_spec.rb +++ b/spec/controllers/projects/todos_controller_spec.rb @@ -5,6 +5,7 @@ require('spec_helper') RSpec.describe Projects::TodosController do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } let(:design) { create(:design, project: project, issue: issue) } diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 1d32f607987..bc903830304 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -10,6 +10,7 @@ RSpec.describe ProjectsController do let_it_be(:project, reload: true) { create(:project, service_desk_enabled: false) } let_it_be(:public_project) { create(:project, :public) } let_it_be(:user) { create(:user) } + let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } @@ -548,6 +549,7 @@ RSpec.describe ProjectsController do describe '#housekeeping' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, group: group) } + let(:housekeeping) { Repositories::HousekeepingService.new(project) } context 'when authenticated as owner' do @@ -1097,6 +1099,7 @@ RSpec.describe ProjectsController do context 'state filter on references' do let_it_be(:issue) { create(:issue, :closed, project: public_project) } + let(:merge_request) { create(:merge_request, :closed, target_project: public_project) } it 'renders JSON body with state filter for issues' do diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index feb0b0b992f..20ae569322c 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -90,7 +90,7 @@ RSpec.describe 'Issue Boards new issue', :js do wait_for_requests - expect(page).to have_selector('.issue-boards-sidebar') + expect(page).to have_selector('[data-testid="issue-boards-sidebar"]') end it 'successfuly loads labels to be added to newly created issue' do @@ -109,7 +109,7 @@ RSpec.describe 'Issue Boards new issue', :js do find('.board-card').click end - page.within(first('.issue-boards-sidebar')) do + page.within(first('[data-testid="issue-boards-sidebar"]')) do find('.labels [data-testid="edit-button"]').click wait_for_requests diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb index 38deee547a3..d31a7977f66 100644 --- a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb @@ -8,6 +8,7 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js let_it_be(:user1) { create(:user, name: 'John Doe') } let_it_be(:group) { create(:group) } + let(:new_member) { create(:user, name: 'Mary Jane') } before do diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 33afa5d04e5..22785adae4a 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -244,6 +244,7 @@ RSpec.describe 'Group' do describe 'group edit', :js do let_it_be(:group) { create(:group, :public) } + let(:path) { edit_group_path(group) } let(:new_name) { 'new-name' } @@ -289,6 +290,7 @@ RSpec.describe 'Group' do describe 'group page with markdown description' do let_it_be(:group) { create(:group) } + let(:path) { group_path(group) } before do diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index ec61cab08aa..7c18f0a438e 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -24,7 +24,7 @@ describe('BoardContentSidebar', () => { issuableType: 'issue', }, getters: { - activeIssue: () => { + activeBoardItem: () => { return { ...mockIssue, epic: null }; }, groupPathForActiveIssue: () => mockIssueGroupPath, diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js index 32499bd5480..24fcdd528d5 100644 --- a/spec/frontend/boards/components/board_form_spec.js +++ b/spec/frontend/boards/components/board_form_spec.js @@ -226,7 +226,7 @@ describe('BoardForm', () => { it('passes correct primary action text and variant', () => { expect(findModalActionPrimary().text).toBe('Save changes'); - expect(findModalActionPrimary().attributes[0].variant).toBe('info'); + expect(findModalActionPrimary().attributes[0].variant).toBe('confirm'); }); it('does not render delete confirmation message', () => { diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js index 98ac211238c..153d0640b23 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js @@ -64,7 +64,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper(); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => TEST_LABELS); + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => TEST_LABELS); findLabelsSelect().vm.$emit('updateSelectedLabels', TEST_LABELS_PAYLOAD); store.state.boardItems[TEST_ISSUE.id].labels = TEST_LABELS; await wrapper.vm.$nextTick(); @@ -76,7 +76,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { }); it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({ addLabelIds: TEST_LABELS.map((label) => label.id), projectPath: 'gitlab-org/test-subgroup/gitlab-test', removeLabelIds: [], @@ -94,13 +94,13 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper({ labels: TEST_LABELS }); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => expectedLabels); + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => expectedLabels); findLabelsSelect().vm.$emit('updateSelectedLabels', testLabelsPayload); await wrapper.vm.$nextTick(); }); it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({ addLabelIds: [5, 7], removeLabelIds: [6], projectPath: 'gitlab-org/test-subgroup/gitlab-test', @@ -114,13 +114,13 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper({ labels: [testLabel] }); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => {}); + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {}); }); it('commits change to the server', () => { wrapper.find(GlLabel).vm.$emit('close', testLabel); - expect(wrapper.vm.setActiveIssueLabels).toHaveBeenCalledWith({ + expect(wrapper.vm.setActiveBoardItemLabels).toHaveBeenCalledWith({ removeLabelIds: [getIdFromGraphQLId(testLabel.id)], projectPath: 'gitlab-org/test-subgroup/gitlab-test', }); @@ -131,7 +131,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { beforeEach(async () => { createWrapper({ labels: TEST_LABELS }); - jest.spyOn(wrapper.vm, 'setActiveIssueLabels').mockImplementation(() => { + jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => { throw new Error(['failed mutation']); }); findLabelsSelect().vm.$emit('updateSelectedLabels', [{ id: '?' }]); diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js index cfd7f32b2cc..1d9b6eb31ab 100644 --- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js +++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js @@ -20,10 +20,10 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = const findToggle = () => wrapper.find(GlToggle); const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon); - const createComponent = (activeIssue = { ...mockActiveIssue }) => { + const createComponent = (activeBoardItem = { ...mockActiveIssue }) => { store = createStore(); - store.state.boardItems = { [activeIssue.id]: activeIssue }; - store.state.activeId = activeIssue.id; + store.state.boardItems = { [activeBoardItem.id]: activeBoardItem }; + store.state.activeId = activeBoardItem.id; wrapper = mount(BoardSidebarSubscription, { localVue, @@ -91,8 +91,8 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () = describe('Board sidebar subscription component `behavior`', () => { const mockSetActiveIssueSubscribed = (subscribedState) => { jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { - store.commit(types.UPDATE_ISSUE_BY_ID, { - issueId: mockActiveIssue.id, + store.commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: mockActiveIssue.id, prop: 'subscribed', value: subscribedState, }); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 7511a4ad2cb..8cb2c35d503 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -5,6 +5,7 @@ import { formatListIssues, formatBoardLists, formatIssueInput, + formatIssue, } from '~/boards/boards_util'; import { inactiveId, ISSUABLE } from '~/boards/constants'; import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql'; @@ -12,6 +13,7 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql' import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { mockLists, mockListsById, @@ -802,11 +804,11 @@ describe('setAssignees', () => { testAction( actions.setAssignees, [node], - { activeIssue: { iid, referencePath: refPath }, commit: () => {} }, + { activeBoardItem: { iid, referencePath: refPath }, commit: () => {} }, [ { - type: 'UPDATE_ISSUE_BY_ID', - payload: { prop: 'assignees', issueId: undefined, value: [node] }, + type: 'UPDATE_BOARD_ITEM_BY_ID', + payload: { prop: 'assignees', itemId: undefined, value: [node] }, }, ], [], @@ -816,7 +818,43 @@ describe('setAssignees', () => { }); }); -describe('createNewIssue', () => { +describe('addListItem', () => { + it('should commit ADD_BOARD_ITEM_TO_LIST and UPDATE_BOARD_ITEM mutations', () => { + const payload = { + list: mockLists[0], + item: mockIssue, + position: 0, + }; + + testAction(actions.addListItem, payload, {}, [ + { + type: types.ADD_BOARD_ITEM_TO_LIST, + payload: { + listId: mockLists[0].id, + itemId: mockIssue.id, + atIndex: 0, + }, + }, + { type: types.UPDATE_BOARD_ITEM, payload: mockIssue }, + ]); + }); +}); + +describe('removeListItem', () => { + it('should commit REMOVE_BOARD_ITEM_FROM_LIST and REMOVE_BOARD_ITEM mutations', () => { + const payload = { + listId: mockLists[0].id, + itemId: mockIssue.id, + }; + + testAction(actions.removeListItem, payload, {}, [ + { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload }, + { type: types.REMOVE_BOARD_ITEM, payload: mockIssue.id }, + ]); + }); +}); + +describe('addListNewIssue', () => { const state = { boardType: 'group', fullPath: 'gitlab-org/gitlab', @@ -843,19 +881,7 @@ describe('createNewIssue', () => { }, }; - it('should return issue from API on success', async () => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { - createIssue: { - issue: mockIssue, - errors: [], - }, - }, - }); - - const result = await actions.createNewIssue({ state }, mockIssue); - expect(result).toEqual(mockIssue); - }); + const fakeList = { id: 'gid://gitlab/List/123' }; it('should add board scope to the issue being created', async () => { jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ @@ -867,7 +893,11 @@ describe('createNewIssue', () => { }, }); - await actions.createNewIssue({ state: stateWithBoardConfig }, mockIssue); + await actions.addListNewIssue( + { dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig }, + { issueInput: mockIssue, list: fakeList }, + ); + expect(gqlClient.mutate).toHaveBeenCalledWith({ mutation: issueCreateMutation, variables: { @@ -894,7 +924,11 @@ describe('createNewIssue', () => { const payload = formatIssueInput(issue, stateWithBoardConfig.boardConfig); - await actions.createNewIssue({ state: stateWithBoardConfig }, issue); + await actions.addListNewIssue( + { dispatch: jest.fn(), commit: jest.fn(), state: stateWithBoardConfig }, + { issueInput: issue, list: fakeList }, + ); + expect(gqlClient.mutate).toHaveBeenCalledWith({ mutation: issueCreateMutation, variables: { @@ -905,51 +939,92 @@ describe('createNewIssue', () => { expect(payload.assigneeIds).toEqual(['gid://gitlab/User/1', 'gid://gitlab/User/2']); }); - it('should commit CREATE_ISSUE_FAILURE mutation when API returns an error', (done) => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { - createIssue: { - issue: mockIssue, - errors: [{ foo: 'bar' }], + describe('when issue creation mutation request succeeds', () => { + it('dispatches a correct set of mutations', () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + createIssue: { + issue: mockIssue, + errors: [], + }, }, - }, + }); + + testAction({ + action: actions.addListNewIssue, + payload: { + issueInput: mockIssue, + list: fakeList, + placeholderId: 'tmp', + }, + state, + expectedActions: [ + { + type: 'addListItem', + payload: { + list: fakeList, + item: formatIssue({ ...mockIssue, id: 'tmp' }), + position: 0, + }, + }, + { type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } }, + { + type: 'addListItem', + payload: { + list: fakeList, + item: formatIssue({ ...mockIssue, id: getIdFromGraphQLId(mockIssue.id) }), + position: 0, + }, + }, + ], + }); }); - - const payload = mockIssue; - - testAction( - actions.createNewIssue, - payload, - state, - [{ type: types.CREATE_ISSUE_FAILURE }], - [], - done, - ); }); -}); -describe('addListIssue', () => { - it('should commit ADD_ISSUE_TO_LIST mutation', (done) => { - const payload = { - list: mockLists[0], - issue: mockIssue, - position: 0, - }; - - testAction( - actions.addListIssue, - payload, - {}, - [{ type: types.ADD_ISSUE_TO_LIST, payload }], - [], - done, - ); + describe('when issue creation mutation request fails', () => { + it('dispatches a correct set of mutations', () => { + jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ + data: { + createIssue: { + issue: mockIssue, + errors: [{ foo: 'bar' }], + }, + }, + }); + + testAction({ + action: actions.addListNewIssue, + payload: { + issueInput: mockIssue, + list: fakeList, + placeholderId: 'tmp', + }, + state, + expectedActions: [ + { + type: 'addListItem', + payload: { + list: fakeList, + item: formatIssue({ ...mockIssue, id: 'tmp' }), + position: 0, + }, + }, + { type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } }, + ], + expectedMutations: [ + { + type: types.SET_ERROR, + payload: 'An error occurred while creating the issue. Please try again.', + }, + ], + }); + }); }); }); describe('setActiveIssueLabels', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testLabelIds = labels.map((label) => label.id); const input = { addLabelIds: testLabelIds, @@ -963,7 +1038,7 @@ describe('setActiveIssueLabels', () => { .mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'labels', value: labels, }; @@ -974,7 +1049,7 @@ describe('setActiveIssueLabels', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -994,7 +1069,7 @@ describe('setActiveIssueLabels', () => { describe('setActiveIssueDueDate', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testDueDate = '2020-02-20'; const input = { dueDate: testDueDate, @@ -1014,7 +1089,7 @@ describe('setActiveIssueDueDate', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'dueDate', value: testDueDate, }; @@ -1025,7 +1100,7 @@ describe('setActiveIssueDueDate', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1045,7 +1120,7 @@ describe('setActiveIssueDueDate', () => { describe('setActiveIssueSubscribed', () => { const state = { boardItems: { [mockActiveIssue.id]: mockActiveIssue } }; - const getters = { activeIssue: mockActiveIssue }; + const getters = { activeBoardItem: mockActiveIssue }; const subscribedState = true; const input = { subscribedState, @@ -1065,7 +1140,7 @@ describe('setActiveIssueSubscribed', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'subscribed', value: subscribedState, }; @@ -1076,7 +1151,7 @@ describe('setActiveIssueSubscribed', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1096,7 +1171,7 @@ describe('setActiveIssueSubscribed', () => { describe('setActiveIssueMilestone', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testMilestone = { ...mockMilestone, id: 'gid://gitlab/Milestone/1', @@ -1119,7 +1194,7 @@ describe('setActiveIssueMilestone', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'milestone', value: testMilestone, }; @@ -1130,7 +1205,7 @@ describe('setActiveIssueMilestone', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1150,7 +1225,7 @@ describe('setActiveIssueMilestone', () => { describe('setActiveIssueTitle', () => { const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeIssue: mockIssue }; + const getters = { activeBoardItem: mockIssue }; const testTitle = 'Test Title'; const input = { title: testTitle, @@ -1170,7 +1245,7 @@ describe('setActiveIssueTitle', () => { }); const payload = { - issueId: getters.activeIssue.id, + itemId: getters.activeBoardItem.id, prop: 'title', value: testTitle, }; @@ -1181,7 +1256,7 @@ describe('setActiveIssueTitle', () => { { ...state, ...getters }, [ { - type: types.UPDATE_ISSUE_BY_ID, + type: types.UPDATE_BOARD_ITEM_BY_ID, payload, }, ], @@ -1325,7 +1400,7 @@ describe('toggleBoardItemMultiSelection', () => { testAction( actions.toggleBoardItemMultiSelection, boardItem2, - { activeId: mockActiveIssue.id, activeIssue: mockActiveIssue, selectedBoardItems: [] }, + { activeId: mockActiveIssue.id, activeBoardItem: mockActiveIssue, selectedBoardItems: [] }, [ { type: types.ADD_BOARD_ITEM_TO_SELECTION, diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js index 0541c40061c..6114ba0af5f 100644 --- a/spec/frontend/boards/stores/getters_spec.js +++ b/spec/frontend/boards/stores/getters_spec.js @@ -88,7 +88,7 @@ describe('Boards - Getters', () => { }); }); - describe('activeIssue', () => { + describe('activeBoardItem', () => { it.each` id | expected ${'1'} | ${'issue'} @@ -96,7 +96,7 @@ describe('Boards - Getters', () => { `('returns $expected when $id is passed to state', ({ id, expected }) => { const state = { boardItems: { 1: 'issue' }, activeId: id }; - expect(getters.activeIssue(state)).toEqual(expected); + expect(getters.activeBoardItem(state)).toEqual(expected); }); }); @@ -105,14 +105,14 @@ describe('Boards - Getters', () => { const mockActiveIssue = { referencePath: 'gitlab-org/gitlab-test#1', }; - expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual( + expect(getters.groupPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual( 'gitlab-org', ); }); it('returns empty string as group path when active issue is an empty object', () => { const mockActiveIssue = {}; - expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(''); + expect(getters.groupPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual(''); }); }); @@ -121,14 +121,16 @@ describe('Boards - Getters', () => { const mockActiveIssue = { referencePath: 'gitlab-org/gitlab-test#1', }; - expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual( + expect(getters.projectPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual( 'gitlab-org/gitlab-test', ); }); it('returns empty string as project path when active issue is an empty object', () => { const mockActiveIssue = {}; - expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(''); + expect(getters.projectPathForActiveIssue({}, { activeBoardItem: mockActiveIssue })).toEqual( + '', + ); }); }); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index d781f403b93..ded69fc0e0f 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import { issuableTypes } from '~/boards/constants'; import * as types from '~/boards/stores/mutation_types'; import mutations from '~/boards/stores/mutations'; @@ -9,6 +10,7 @@ import { mockIssue2, mockGroupProjects, labels, + mockList, } from '../mock_data'; const expectNotImplemented = (action) => { @@ -25,6 +27,14 @@ describe('Board Store Mutations', () => { 'gid://gitlab/List/2': mockLists[1], }; + const setBoardsListsState = () => { + state = cloneDeep({ + ...state, + boardItemsByListId: { 'gid://gitlab/List/1': [mockIssue.id] }, + boardLists: { 'gid://gitlab/List/1': mockList }, + }); + }; + beforeEach(() => { state = defaultState(); }); @@ -335,7 +345,7 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.REQUEST_ADD_ISSUE); }); - describe('UPDATE_ISSUE_BY_ID', () => { + describe('UPDATE_BOARD_ITEM_BY_ID', () => { const issueId = '1'; const prop = 'id'; const value = '2'; @@ -353,8 +363,8 @@ describe('Board Store Mutations', () => { describe('when the issue is in state', () => { it('updates the property of the correct issue', () => { - mutations.UPDATE_ISSUE_BY_ID(state, { - issueId, + mutations.UPDATE_BOARD_ITEM_BY_ID(state, { + itemId: issueId, prop, value, }); @@ -366,8 +376,8 @@ describe('Board Store Mutations', () => { describe('when the issue is not in state', () => { it('throws an error', () => { expect(() => { - mutations.UPDATE_ISSUE_BY_ID(state, { - issueId: '3', + mutations.UPDATE_BOARD_ITEM_BY_ID(state, { + itemId: '3', prop, value, }); @@ -467,6 +477,27 @@ describe('Board Store Mutations', () => { }); }); + describe('UPDATE_BOARD_ITEM', () => { + it('updates the given issue in state.boardItems', () => { + const updatedIssue = { id: 'some_gid', foo: 'bar' }; + state = { boardItems: { some_gid: { id: 'some_gid' } } }; + + mutations.UPDATE_BOARD_ITEM(state, updatedIssue); + + expect(state.boardItems.some_gid).toEqual(updatedIssue); + }); + }); + + describe('REMOVE_BOARD_ITEM', () => { + it('removes the given issue from state.boardItems', () => { + state = { boardItems: { some_gid: {}, some_gid2: {} } }; + + mutations.REMOVE_BOARD_ITEM(state, 'some_gid'); + + expect(state.boardItems).toEqual({ some_gid2: {} }); + }); + }); + describe('REQUEST_UPDATE_ISSUE', () => { expectNotImplemented(mutations.REQUEST_UPDATE_ISSUE); }); @@ -479,85 +510,89 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR); }); - describe('CREATE_ISSUE_FAILURE', () => { - it('sets error message on state', () => { - mutations.CREATE_ISSUE_FAILURE(state); + describe('ADD_BOARD_ITEM_TO_LIST', () => { + beforeEach(() => { + setBoardsListsState(); + }); + + it.each([ + [ + 'at position 0 by default', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + [ + 'at a given position', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + atIndex: 1, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], + [ + "below the issue with id of 'moveBeforeId'", + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + moveBeforeId: mockIssue.id, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], + [ + "above the issue with id of 'moveAfterId'", + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + moveAfterId: mockIssue.id, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + ])(`inserts an item into a list %s`, (_, { payload, listState }) => { + mutations.ADD_BOARD_ITEM_TO_LIST(state, payload); - expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); + expect(state.boardItemsByListId[payload.listId]).toEqual(listState); }); - }); - - describe('ADD_ISSUE_TO_LIST', () => { - it('adds issue to issues state and issue id in list in boardItemsByListId', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id], - }; - const issues = { - 1: mockIssue, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardItems: issues, - boardLists: initialBoardListsState, - }; + it("updates the list's items count", () => { expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); - mutations.ADD_ISSUE_TO_LIST(state, { list: mockLists[0], issue: mockIssue2 }); + mutations.ADD_BOARD_ITEM_TO_LIST(state, { + itemId: mockIssue2.id, + listId: mockList.id, + }); - expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue2.id); - expect(state.boardItems[mockIssue2.id]).toEqual(mockIssue2); expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(2); }); }); - describe('ADD_ISSUE_TO_LIST_FAILURE', () => { - it('removes issue id from list in boardItemsByListId and sets error message', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - }; - const issues = { - 1: mockIssue, - 2: mockIssue2, - }; - - state = { - ...state, - boardItemsByListId: listIssues, - boardItems: issues, - boardLists: initialBoardListsState, - }; - - mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); - - expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); - expect(state.error).toBe('An error occurred while creating the issue. Please try again.'); + describe('REMOVE_BOARD_ITEM_FROM_LIST', () => { + beforeEach(() => { + setBoardsListsState(); }); - }); - - describe('REMOVE_ISSUE_FROM_LIST', () => { - it('removes issue id from list in boardItemsByListId and deletes issue from state', () => { - const listIssues = { - 'gid://gitlab/List/1': [mockIssue.id, mockIssue2.id], - }; - const issues = { - 1: mockIssue, - 2: mockIssue2, - }; - state = { - ...state, - boardItemsByListId: listIssues, - boardItems: issues, - boardLists: initialBoardListsState, - }; + it("removes an item from a list and updates the list's items count", () => { + expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(1); + expect(state.boardItemsByListId['gid://gitlab/List/1']).toContain(mockIssue.id); - mutations.ADD_ISSUE_TO_LIST_FAILURE(state, { list: mockLists[0], issueId: mockIssue2.id }); + mutations.REMOVE_BOARD_ITEM_FROM_LIST(state, { + itemId: mockIssue.id, + listId: mockList.id, + }); - expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue2.id); - expect(state.boardItems).not.toContain(mockIssue2); + expect(state.boardItemsByListId['gid://gitlab/List/1']).not.toContain(mockIssue.id); + expect(state.boardLists['gid://gitlab/List/1'].issuesCount).toBe(0); }); }); diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 9ca5aeeea58..900dfec38e2 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -321,4 +321,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do it { is_expected.to be_falsey } end end + + describe '#increment_pipeline_failure_reason_counter' do + let(:command) { described_class.new } + let(:reason) { :size_limit_exceeded } + + subject { command.increment_pipeline_failure_reason_counter(reason) } + + it 'increments the error metric' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { subject }.to change { counter.get(reason: reason.to_s) }.by(1) + end + + context 'when the reason is nil' do + let(:reason) { nil } + + it 'increments the error metric with unknown_failure' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { subject }.to change { counter.get(reason: 'unknown_failure') }.by(1) + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb index 78363be7f36..23cdec61bb3 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -11,7 +11,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do let(:save_incompleted) { false } let(:command) do - double(:command, + Gitlab::Ci::Pipeline::Chain::Command.new( project: project, pipeline_seed: pipeline_seed, save_incompleted: save_incompleted @@ -49,6 +49,11 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do expect(pipeline.deployments_limit_exceeded?).to be true end + + it 'calls increment_pipeline_failure_reason_counter' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { perform }.to change { counter.get(reason: 'deployments_limit_exceeded') }.by(1) + end end context 'when not saving incomplete pipelines' do @@ -71,6 +76,12 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.']) end + + it 'increments the error metric' do + expect(command).to receive(:increment_pipeline_failure_reason_counter).with(:deployments_limit_exceeded) + + perform + end end it 'logs the error' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 53d8ca740b5..62de4d2e96d 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -96,6 +96,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do it 'wastes pipeline iid' do expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 end + + it 'increments the error metric' do + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { run_chain }.to change { counter.get(reason: 'unknown_failure') }.by(1) + end end describe 'pipeline protect' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 161e02ad58d..6d96ef2f245 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2635,6 +2635,37 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do expect(latest_status).to eq %w(canceled canceled) end end + + context 'preloading relations' do + let(:pipeline1) { create(:ci_empty_pipeline, :created) } + let(:pipeline2) { create(:ci_empty_pipeline, :created) } + + before do + create(:ci_build, :pending, pipeline: pipeline1) + create(:generic_commit_status, :pending, pipeline: pipeline1) + + create(:ci_build, :pending, pipeline: pipeline2) + create(:ci_build, :pending, pipeline: pipeline2) + create(:generic_commit_status, :pending, pipeline: pipeline2) + create(:generic_commit_status, :pending, pipeline: pipeline2) + create(:generic_commit_status, :pending, pipeline: pipeline2) + end + + it 'preloads relations for each build to avoid N+1 queries' do + control1 = ActiveRecord::QueryRecorder.new do + pipeline1.cancel_running + end + + control2 = ActiveRecord::QueryRecorder.new do + pipeline2.cancel_running + end + + extra_update_queries = 3 # transition ... => :canceled + extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types + + expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries) + end + end end describe '#retry_failed' do @@ -3836,6 +3867,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do pipeline.drop end end + + context 'with failure_reason' do + let(:pipeline) { create(:ci_pipeline, :running) } + let(:failure_reason) { 'config_error' } + let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') } + + it 'increments the counter with the failure_reason' do + expect { pipeline.drop!(failure_reason) }.to change { counter.get(reason: failure_reason) }.by(1) + end + end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 35aa9b05b70..3bdeaff475a 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -629,30 +629,45 @@ RSpec.describe CommitStatus do end end - describe 'set failure_reason when drop' do + describe '#drop' do let(:commit_status) { create(:commit_status, :created) } + let(:counter) { Gitlab::Metrics.counter(:gitlab_ci_job_failure_reasons, 'desc') } + let(:failure_reason) { reason.to_s } subject do commit_status.drop!(reason) commit_status end + shared_examples 'incrementing failure reason counter' do + it 'increments the counter with the failure_reason' do + expect { subject }.to change { counter.get(reason: failure_reason) }.by(1) + end + end + context 'when failure_reason is nil' do let(:reason) { } + let(:failure_reason) { 'unknown_failure' } it { is_expected.to be_unknown_failure } + + it_behaves_like 'incrementing failure reason counter' end context 'when failure_reason is script_failure' do let(:reason) { :script_failure } it { is_expected.to be_script_failure } + + it_behaves_like 'incrementing failure reason counter' end context 'when failure_reason is unmet_prerequisites' do let(:reason) { :unmet_prerequisites } it { is_expected.to be_unmet_prerequisites } + + it_behaves_like 'incrementing failure reason counter' end end diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb index 87c1874ec41..3d4dc3f69c9 100644 --- a/spec/serializers/build_artifact_entity_spec.rb +++ b/spec/serializers/build_artifact_entity_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' RSpec.describe BuildArtifactEntity do - let(:job) { create(:ci_build) } - let(:artifact) { create(:ci_job_artifact, :codequality, expire_at: 1.hour.from_now, job: job) } + let_it_be(:job) { create(:ci_build) } + let_it_be(:artifact) { create(:ci_job_artifact, :codequality, expire_at: 1.hour.from_now, job: job) } + + let(:options) { { request: double } } let(:entity) do - described_class.new(artifact, request: double) + described_class.represent(artifact, options) end describe '#as_json' do @@ -46,5 +48,15 @@ RSpec.describe BuildArtifactEntity do expect(subject[:browse_path]).to be_present end end + + context 'when project is specified in options' do + let(:options) { super().merge(project: job.project) } + + it 'doesnt get a project from the artifact' do + expect(artifact).not_to receive(:project) + + subject + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 35f4b97df0a..98c85234fe7 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -71,19 +71,21 @@ RSpec.describe Ci::CreatePipelineService do end it 'increments the prometheus counter' do - expect(Gitlab::Metrics).to receive(:counter) - .with(:pipelines_created_total, "Counter of pipelines created") - .and_call_original - allow(Gitlab::Metrics).to receive(:counter).and_call_original # allow other counters + counter = spy('pipeline created counter') + + allow(Gitlab::Ci::Pipeline::Metrics) + .to receive(:pipelines_created_counter).and_return(counter) pipeline + + expect(counter).to have_received(:increment) end it 'records pipeline size in a prometheus histogram' do histogram = spy('pipeline size histogram') allow(Gitlab::Ci::Pipeline::Metrics) - .to receive(:new).and_return(histogram) + .to receive(:pipeline_size_histogram).and_return(histogram) execute_service @@ -580,6 +582,13 @@ RSpec.describe Ci::CreatePipelineService do it_behaves_like 'a failed pipeline' + it 'increments the error metric' do + stub_ci_pipeline_yaml_file(ci_yaml) + + counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') + expect { execute_service }.to change { counter.get(reason: 'config_error') }.by(1) + end + context 'when receive git commit' do before do allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index e02536fd07f..254bd19c808 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -10,6 +10,14 @@ RSpec.describe Ci::ProcessPipelineService do create(:ci_empty_pipeline, ref: 'master', project: project) end + let(:pipeline_processing_events_counter) { double(increment: true) } + let(:legacy_update_jobs_counter) { double(increment: true) } + + let(:metrics) do + double(pipeline_processing_events_counter: pipeline_processing_events_counter, + legacy_update_jobs_counter: legacy_update_jobs_counter) + end + subject { described_class.new(pipeline) } before do @@ -17,22 +25,13 @@ RSpec.describe Ci::ProcessPipelineService do stub_not_protect_default_branch project.add_developer(user) + + allow(subject).to receive(:metrics).and_return(metrics) end describe 'processing events counter' do - let(:metrics) { double('pipeline metrics') } - let(:counter) { double('events counter') } - - before do - allow(subject) - .to receive(:metrics).and_return(metrics) - allow(metrics) - .to receive(:pipeline_processing_events_counter) - .and_return(counter) - end - it 'increments processing events counter' do - expect(counter).to receive(:increment) + expect(pipeline_processing_events_counter).to receive(:increment) subject.execute end @@ -64,33 +63,22 @@ RSpec.describe Ci::ProcessPipelineService do expect(all_builds.retried).to contain_exactly(build_retried) end - context 'counter ci_legacy_update_jobs_as_retried_total' do - let(:counter) { double(increment: true) } + it 'increments the counter' do + expect(legacy_update_jobs_counter).to receive(:increment) + subject.execute + end + + context 'when the previous build has already retried column true' do before do - allow(Gitlab::Metrics).to receive(:counter).and_call_original - allow(Gitlab::Metrics).to receive(:counter) - .with(:ci_legacy_update_jobs_as_retried_total, anything) - .and_return(counter) + build_retried.update_columns(retried: true) end - it 'increments the counter' do - expect(counter).to receive(:increment) + it 'does not increment the counter' do + expect(legacy_update_jobs_counter).not_to receive(:increment) subject.execute end - - context 'when the previous build has already retried column true' do - before do - build_retried.update_columns(retried: true) - end - - it 'does not increment the counter' do - expect(counter).not_to receive(:increment) - - subject.execute - end - end end end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index 558536a80c6..429efbe6ba0 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -8,19 +8,19 @@ RSpec.shared_examples 'issue boards sidebar' do end it 'shows sidebar when clicking issue' do - expect(page).to have_selector('.issue-boards-sidebar') + expect(page).to have_selector('[data-testid="issue-boards-sidebar"]') end it 'closes sidebar when clicking issue' do - expect(page).to have_selector('.issue-boards-sidebar') + expect(page).to have_selector('[data-testid="issue-boards-sidebar"]') first_card.click - expect(page).not_to have_selector('.issue-boards-sidebar') + expect(page).not_to have_selector('[data-testid="issue-boards-sidebar"]') end it 'shows issue details when sidebar is open', :aggregate_failures do - page.within('.issue-boards-sidebar') do + page.within('[data-testid="issue-boards-sidebar"]') do expect(page).to have_content(issue.title) expect(page).to have_content(issue.to_reference) end @@ -28,7 +28,7 @@ RSpec.shared_examples 'issue boards sidebar' do context 'when clicking close button' do before do - find("[data-testid='sidebar-drawer'] .gl-drawer-close-button").click + find('[data-testid="issue-boards-sidebar"] .gl-drawer-close-button').click end it 'unhighlights the active issue card' do @@ -37,7 +37,7 @@ RSpec.shared_examples 'issue boards sidebar' do end it 'closes sidebar when clicking close button' do - expect(page).not_to have_selector('.issue-boards-sidebar') + expect(page).not_to have_selector('[data-testid="issue-boards-sidebar"]') end end |