diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 15:10:43 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 15:10:43 +0300 |
commit | bf18f3295b550c564086efd0a32d9a25435ce216 (patch) | |
tree | 9ea92eefd45aa38a15152fb28c24d526c1525a5f /app | |
parent | 3f96425b0b9f0b4885b70db01dcd76b311ea87ab (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
38 files changed, 716 insertions, 353 deletions
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue new file mode 100644 index 00000000000..11d7fa8d65b --- /dev/null +++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue @@ -0,0 +1,233 @@ +<script> +import { + GlAlert, + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import { s__, __ } from '~/locale'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import currentUserNamespace from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; + +export const i18n = { + SELECT_A_NAMESPACE: __('Select a new namespace'), + GROUPS: __('Groups'), + USERS: __('Users'), + ERROR_MESSAGE: s__( + 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.', + ), + ALERT_DISMISS_LABEL: __('Dismiss'), +}; + +export default { + name: 'TransferLocations', + components: { + GlAlert, + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, + }, + inject: ['resourceId'], + props: { + value: { + type: Object, + required: false, + default: null, + }, + groupTransferLocationsApiMethod: { + type: Function, + required: true, + }, + }, + initialTransferLocationsLoaded: false, + data() { + return { + searchTerm: '', + userTransferLocations: [], + groupTransferLocations: [], + isLoading: false, + isSearchLoading: false, + hasError: false, + page: 1, + totalPages: 1, + }; + }, + computed: { + hasUserTransferLocations() { + return this.userTransferLocations.length; + }, + hasGroupTransferLocations() { + return this.groupTransferLocations.length; + }, + selectedText() { + return this.value?.humanName || i18n.SELECT_A_NAMESPACE; + }, + hasNextPageOfGroups() { + return this.page < this.totalPages; + }, + }, + watch: { + searchTerm() { + this.page = 1; + + this.debouncedSearch(); + }, + }, + methods: { + handleSelect(item) { + this.searchTerm = ''; + this.$emit('input', item); + }, + async handleShow() { + if (this.$options.initialTransferLocationsLoaded) { + return; + } + + this.isLoading = true; + + [this.groupTransferLocations, this.userTransferLocations] = await Promise.all([ + this.getGroupTransferLocations(), + this.getUserTransferLocations(), + ]); + + this.isLoading = false; + this.$options.initialTransferLocationsLoaded = true; + }, + async getGroupTransferLocations() { + try { + const { + data: groupTransferLocations, + headers, + } = await this.groupTransferLocationsApiMethod(this.resourceId, { + page: this.page, + search: this.searchTerm, + }); + + const { totalPages } = parseIntPagination(normalizeHeaders(headers)); + this.totalPages = totalPages; + + return groupTransferLocations.map(({ id, full_name: humanName }) => ({ + id, + humanName, + })); + } catch { + this.handleError(); + + return []; + } + }, + async getUserTransferLocations() { + try { + const { + data: { + currentUser: { namespace }, + }, + } = await this.$apollo.query({ + query: currentUserNamespace, + }); + + if (!namespace) { + return []; + } + + return [ + { + id: getIdFromGraphQLId(namespace.id), + humanName: namespace.fullName, + }, + ]; + } catch { + this.handleError(); + + return []; + } + }, + async handleLoadMoreGroups() { + this.isLoading = true; + this.page += 1; + + const groupTransferLocations = await this.getGroupTransferLocations(); + this.groupTransferLocations.push(...groupTransferLocations); + + this.isLoading = false; + }, + debouncedSearch: debounce(async function debouncedSearch() { + this.isSearchLoading = true; + + this.groupTransferLocations = await this.getGroupTransferLocations(); + + this.isSearchLoading = false; + }, DEBOUNCE_DELAY), + handleError() { + this.hasError = true; + }, + handleAlertDismiss() { + this.hasError = false; + }, + }, + i18n, +}; +</script> +<template> + <div> + <gl-alert + v-if="hasError" + variant="danger" + :dismiss-label="$options.i18n.ALERT_DISMISS_LABEL" + @dismiss="handleAlertDismiss" + >{{ $options.i18n.ERROR_MESSAGE }}</gl-alert + > + <gl-form-group :label="$options.i18n.SELECT_A_NAMESPACE"> + <gl-dropdown :text="selectedText" data-qa-selector="namespaces_list" block @show="handleShow"> + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :is-loading="isSearchLoading" + data-qa-selector="namespaces_list_search" + /> + </template> + <div + v-if="hasUserTransferLocations" + data-qa-selector="namespaces_list_users" + data-testid="user-transfer-locations" + > + <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="item in userTransferLocations" + :key="item.id" + data-qa-selector="namespaces_list_item" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + </div> + <div + v-if="hasGroupTransferLocations" + data-qa-selector="namespaces_list_groups" + data-testid="group-transfer-locations" + > + <gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="item in groupTransferLocations" + :key="item.id" + data-qa-selector="namespaces_list_item" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + </div> + <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" /> + <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="handleLoadMoreGroups" /> + </gl-dropdown> + </gl-form-group> + </div> +</template> diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql new file mode 100644 index 00000000000..d350072425b --- /dev/null +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql @@ -0,0 +1,5 @@ +mutation moveIssue($moveIssueInput: IssueMoveInput!) { + issueMove(input: $moveIssueInput) { + errors + } +} diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue new file mode 100644 index 00000000000..6e287ac3bb7 --- /dev/null +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue @@ -0,0 +1,171 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue'; +import createFlash from '~/flash'; +import { logError } from '~/lib/logger'; +import { s__ } from '~/locale'; +import { + WORK_ITEM_TYPE_ENUM_ISSUE, + WORK_ITEM_TYPE_ENUM_INCIDENT, + WORK_ITEM_TYPE_ENUM_TASK, + WORK_ITEM_TYPE_ENUM_TEST_CASE, +} from '~/work_items/constants'; +import issuableEventHub from '~/issues/list/eventhub'; +import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; +import getIssuesCountQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; +import moveIssueMutation from './graphql/mutations/move_issue.mutation.graphql'; + +export default { + name: 'MoveIssuesButton', + components: { + IssuableMoveDropdown, + GlAlert, + }, + props: { + projectFullPath: { + type: String, + required: true, + }, + projectsFetchPath: { + type: String, + required: true, + }, + }, + data() { + return { + selectedIssuables: [], + moveInProgress: false, + }; + }, + computed: { + cannotMoveTasksWarningTitle() { + if (this.tasksSelected && this.testCasesSelected) { + return s__('Issues|Tasks and test cases can not be moved.'); + } + + if (this.testCasesSelected) { + return s__('Issues|Test cases can not be moved.'); + } + + return s__('Issues|Tasks can not be moved.'); + }, + issuesSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_ISSUE); + }, + incidentsSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_INCIDENT); + }, + tasksSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TASK); + }, + testCasesSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TEST_CASE); + }, + }, + mounted() { + issuableEventHub.$on('issuables:issuableChecked', this.handleIssuableChecked); + }, + beforeDestroy() { + issuableEventHub.$off('issuables:issuableChecked', this.handleIssuableChecked); + }, + methods: { + handleIssuableChecked(issuable, value) { + if (value) { + this.selectedIssuables.push(issuable); + } else { + const index = this.selectedIssuables.indexOf(issuable); + if (index > -1) { + this.selectedIssuables.splice(index, 1); + } + } + }, + moveIssues(targetProject) { + const iids = this.selectedIssuables.reduce((result, issueData) => { + if ( + issueData.type === WORK_ITEM_TYPE_ENUM_ISSUE || + issueData.type === WORK_ITEM_TYPE_ENUM_INCIDENT + ) { + result.push(issueData.iid); + } + return result; + }, []); + + if (iids.length === 0) { + return; + } + + this.moveInProgress = true; + issuableEventHub.$emit('issuables:bulkMoveStarted'); + + const promises = iids.map((id) => { + return this.moveIssue(id, targetProject); + }); + + Promise.all(promises) + .then((promisesResult) => { + let foundError = false; + + for (const promiseResult of promisesResult) { + if (promiseResult.data.issueMove?.errors?.length) { + foundError = true; + logError( + `Error moving issue. Error message: ${promiseResult.data.issueMove.errors[0].message}`, + ); + } + } + + if (!foundError) { + const client = this.$apollo.provider.defaultClient; + client.refetchQueries({ + include: [getIssuesQuery, getIssuesCountQuery], + }); + this.moveInProgress = false; + this.selectedIssuables = []; + issuableEventHub.$emit('issuables:bulkMoveEnded'); + } else { + throw new Error(); + } + }) + .catch(() => { + this.moveInProgress = false; + issuableEventHub.$emit('issuables:bulkMoveEnded'); + + createFlash({ + message: s__(`Issues|There was an error while moving the issues.`), + }); + }); + }, + moveIssue(issueIid, targetProject) { + return this.$apollo.mutate({ + mutation: moveIssueMutation, + variables: { + moveIssueInput: { + projectPath: this.projectFullPath, + iid: issueIid, + targetProjectPath: targetProject.full_path, + }, + }, + }); + }, + }, + i18n: { + dropdownButtonTitle: s__('Issues|Move selected'), + }, +}; +</script> +<template> + <div> + <issuable-move-dropdown + :project-full-path="projectFullPath" + :projects-fetch-path="projectsFetchPath" + :move-in-progress="moveInProgress" + :disabled="!issuesSelected && !incidentsSelected" + :dropdown-header-title="$options.i18n.dropdownButtonTitle" + :dropdown-button-title="$options.i18n.dropdownButtonTitle" + @move-issuable="moveIssues" + /> + <gl-alert v-if="tasksSelected || testCasesSelected" :dismissible="false" variant="warning"> + {{ cannotMoveTasksWarningTitle }} + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js index 4657771353f..b7cb805ee37 100644 --- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js @@ -1,6 +1,9 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { gqlClient } from '../../issues/list/graphql'; import StatusDropdown from './components/status_dropdown.vue'; import SubscriptionsDropdown from './components/subscriptions_dropdown.vue'; +import MoveIssuesButton from './components/move_issues_button.vue'; import issuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; @@ -42,3 +45,31 @@ export function initSubscriptionsDropdown() { render: (createElement) => createElement(SubscriptionsDropdown), }); } + +export function initMoveIssuesButton() { + const el = document.querySelector('.js-move-issues'); + + if (!el) { + return null; + } + + const { dataset } = el; + + Vue.use(VueApollo); + const apolloProvider = new VueApollo({ + defaultClient: gqlClient, + }); + + return new Vue({ + el, + name: 'MoveIssuesRoot', + apolloProvider, + render: (createElement) => + createElement(MoveIssuesButton, { + props: { + projectFullPath: dataset.projectFullPath, + projectsFetchPath: dataset.projectsFetchPath, + }, + }), + }); +} diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js index a33c6ae8030..be61831fc4d 100644 --- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js @@ -46,6 +46,11 @@ export default class IssuableBulkUpdateSidebar { // https://gitlab.com/gitlab-org/gitlab/-/issues/325874 issuableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true)); issuableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState()); + + // These events are connected to the logic inside `move_issues_button.vue`, + // so that only one action can be performed at a time + issuableEventHub.$on('issuables:bulkMoveStarted', () => this.toggleSubmitButtonDisabled(true)); + issuableEventHub.$on('issuables:bulkMoveEnded', () => this.updateFormState()); } initDropdowns() { @@ -89,6 +94,8 @@ export default class IssuableBulkUpdateSidebar { this.updateSelectedIssuableIds(); IssuableBulkUpdateActions.setOriginalDropdownData(); + + issuableEventHub.$emit('issuables:selectionChanged', !noCheckedIssues); } prepForSubmit() { diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index acb6aa93f0f..a110ba658f7 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -565,6 +565,7 @@ export default { bulkUpdateSidebar.initBulkUpdateSidebar('issuable_'); bulkUpdateSidebar.initStatusDropdown(); bulkUpdateSidebar.initSubscriptionsDropdown(); + bulkUpdateSidebar.initMoveIssuesButton(); const usersSelect = await import('~/users_select'); const UsersSelect = usersSelect.default; diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js new file mode 100644 index 00000000000..5ef61727a3d --- /dev/null +++ b/app/assets/javascripts/issues/list/graphql.js @@ -0,0 +1,25 @@ +import produce from 'immer'; +import createDefaultClient from '~/lib/graphql'; +import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; + +const resolvers = { + Mutation: { + reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => { + const variables = JSON.parse(serializedVariables); + const sourceData = cache.readQuery({ query: getIssuesQuery, variables }); + + const data = produce(sourceData, (draftData) => { + const issues = draftData[namespace].issues.nodes.slice(); + const issueToMove = issues[oldIndex]; + issues.splice(oldIndex, 1); + issues.splice(newIndex, 0, issueToMove); + + draftData[namespace].issues.nodes = issues; + }); + + cache.writeQuery({ query: getIssuesQuery, variables, data }); + }, + }, +}; + +export const gqlClient = createDefaultClient(resolvers); diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js index 93333c31b34..569ca006af5 100644 --- a/app/assets/javascripts/issues/list/index.js +++ b/app/assets/javascripts/issues/list/index.js @@ -1,12 +1,11 @@ -import produce from 'immer'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; -import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue'; +import { gqlClient } from './graphql'; export function mountJiraIssuesListApp() { const el = document.querySelector('.js-jira-issues-import-status'); @@ -56,26 +55,6 @@ export function mountIssuesListApp() { Vue.use(VueApollo); Vue.use(VueRouter); - const resolvers = { - Mutation: { - reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => { - const variables = JSON.parse(serializedVariables); - const sourceData = cache.readQuery({ query: getIssuesQuery, variables }); - - const data = produce(sourceData, (draftData) => { - const issues = draftData[namespace].issues.nodes.slice(); - const issueToMove = issues[oldIndex]; - issues.splice(oldIndex, 1); - issues.splice(newIndex, 0, issueToMove); - - draftData[namespace].issues.nodes = issues; - }); - - cache.writeQuery({ query: getIssuesQuery, variables, data }); - }, - }, - }; - const { autocompleteAwardEmojisPath, calendarPath, @@ -125,7 +104,7 @@ export function mountIssuesListApp() { el, name: 'IssuesListRoot', apolloProvider: new VueApollo({ - defaultClient: createDefaultClient(resolvers), + defaultClient: gqlClient, }), router: new VueRouter({ base: window.location.pathname, diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue index 4941f22230b..ed5466ff99c 100644 --- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue +++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue @@ -219,7 +219,6 @@ export default { :empty-message="$options.i18n.empty.merge" :keep-component-mounted="false" :is-empty="isEmpty" - :is-invalid="isInvalid" :is-unavailable="isLintUnavailable" :title="$options.i18n.tabMergedYaml" lazy diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index 318940478a8..ebfc7d312b4 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -45,18 +45,17 @@ export default { }; }, update({ project: { branchRules } }) { - this.branchProtection = branchRules.nodes.find( - (rule) => rule.name === this.branch, - )?.branchProtection; + const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch); + this.branchProtection = branchRule?.branchProtection; + this.approvalRules = branchRule?.approvalRules; }, }, }, data() { return { branch: getParameterByName(BRANCH_PARAM_NAME), - branchProtection: { - approvalRules: {}, - }, + branchProtection: {}, + approvalRules: {}, }; }, computed: { @@ -104,7 +103,7 @@ export default { : this.$options.i18n.branchNameOrPattern; }, approvals() { - return this.branchProtection?.approvalRules?.nodes || []; + return this.approvalRules?.nodes || []; }, }, methods: { diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue index 28a1c09fa82..12de136a21a 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue @@ -64,10 +64,10 @@ export default { <template> <div - class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4" + class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4 gl-border-t-1" :class="{ 'gl-border-t-solid': showDivider }" > - <div class="gl-display-flex gl-w-half gl-justify-content-space-between"> + <div class="gl-display-flex gl-w-half gl-justify-content-space-between gl-align-items-center"> <div class="gl-mr-7 gl-w-quarter">{{ title }}</div> <gl-avatars-inline diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql index 3ac165498a1..4ca474a5ceb 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql +++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql @@ -44,6 +44,23 @@ query getBranchRulesDetails($projectPath: ID!) { } } } + approvalRules { + nodes { + id + name + type + approvalsRequired + eligibleApprovers { + nodes { + id + name + username + webUrl + avatarUrl + } + } + } + } } } } diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue index 55420c9c732..886db07a901 100644 --- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue +++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue @@ -1,30 +1,14 @@ <script> -import { GlFormGroup, GlAlert } from '@gitlab/ui'; -import { debounce } from 'lodash'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; -import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import TransferLocations from '~/groups_projects/components/transfer_locations.vue'; import { getTransferLocations } from '~/api/projects_api'; -import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; -import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import { s__, __ } from '~/locale'; -import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql'; export default { name: 'TransferProjectForm', components: { - GlFormGroup, - NamespaceSelect, + TransferLocations, ConfirmDanger, - GlAlert, }, - i18n: { - errorMessage: s__( - 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.', - ), - alertDismissAlert: __('Dismiss'), - }, - inject: ['projectId'], props: { confirmationPhrase: { type: String, @@ -37,146 +21,32 @@ export default { }, data() { return { - userNamespaces: [], - groupNamespaces: [], - initialNamespacesLoaded: false, - selectedNamespace: null, - hasError: false, - isLoading: false, - isSearchLoading: false, - searchTerm: '', - page: 1, - totalPages: 1, + selectedTransferLocation: null, }; }, + computed: { hasSelectedNamespace() { - return Boolean(this.selectedNamespace?.id); + return Boolean(this.selectedTransferLocation?.id); }, - hasNextPageOfGroups() { - return this.page < this.totalPages; + }, + watch: { + selectedTransferLocation(selectedTransferLocation) { + this.$emit('selectTransferLocation', selectedTransferLocation.id); }, }, methods: { - async handleShow() { - if (this.initialNamespacesLoaded) { - return; - } - - this.isLoading = true; - - [this.groupNamespaces, this.userNamespaces] = await Promise.all([ - this.getGroupNamespaces(), - this.getUserNamespaces(), - ]); - - this.isLoading = false; - this.initialNamespacesLoaded = true; - }, - handleSelect(selectedNamespace) { - this.selectedNamespace = selectedNamespace; - this.$emit('selectNamespace', selectedNamespace.id); - }, - async getGroupNamespaces() { - try { - const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, { - page: this.page, - search: this.searchTerm, - }); - - const { totalPages } = parseIntPagination(normalizeHeaders(headers)); - this.totalPages = totalPages; - - return groupNamespaces.map(({ id, full_name: humanName }) => ({ - id, - humanName, - })); - } catch (error) { - this.hasError = true; - - return []; - } - }, - async getUserNamespaces() { - try { - const { - data: { - currentUser: { namespace }, - }, - } = await this.$apollo.query({ - query: currentUserNamespace, - }); - - if (!namespace) { - return []; - } - - return [ - { - id: getIdFromGraphQLId(namespace.id), - humanName: namespace.fullName, - }, - ]; - } catch (error) { - this.hasError = true; - - return []; - } - }, - async handleLoadMoreGroups() { - this.isLoading = true; - this.page += 1; - - const groupNamespaces = await this.getGroupNamespaces(); - this.groupNamespaces.push(...groupNamespaces); - - this.isLoading = false; - }, - debouncedSearch: debounce(async function debouncedSearch() { - this.isSearchLoading = true; - - this.groupNamespaces = await this.getGroupNamespaces(); - - this.isSearchLoading = false; - }, DEBOUNCE_DELAY), - handleSearch(searchTerm) { - this.searchTerm = searchTerm; - this.page = 1; - - this.debouncedSearch(); - }, - handleAlertDismiss() { - this.hasError = false; - }, + getTransferLocations, }, }; </script> <template> <div> - <gl-alert - v-if="hasError" - variant="danger" - :dismiss-label="$options.i18n.alertDismissLabel" - @dismiss="handleAlertDismiss" - >{{ $options.i18n.errorMessage }}</gl-alert - > - <gl-form-group> - <namespace-select - data-testid="transfer-project-namespace" - :full-width="true" - :group-namespaces="groupNamespaces" - :user-namespaces="userNamespaces" - :selected-namespace="selectedNamespace" - :has-next-page-of-groups="hasNextPageOfGroups" - :is-loading="isLoading" - :is-search-loading="isSearchLoading" - :should-filter-namespaces="false" - @select="handleSelect" - @load-more-groups="handleLoadMoreGroups" - @search="handleSearch" - @show="handleShow" - /> - </gl-form-group> + <transfer-locations + v-model="selectedTransferLocation" + data-testid="transfer-project-namespace" + :group-transfer-locations-api-method="getTransferLocations" + /> <confirm-danger :disabled="!hasSelectedNamespace" :phrase="confirmationPhrase" diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js index 89c158a9ba8..7f810e647ae 100644 --- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js +++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js @@ -12,7 +12,7 @@ export default () => { Vue.use(VueApollo); const { - projectId, + projectId: resourceId, targetFormId = null, targetHiddenInputId = null, buttonText: confirmButtonText = '', @@ -27,7 +27,7 @@ export default () => { }), provide: { confirmDangerMessage, - projectId, + resourceId, }, render(createElement) { return createElement(TransferProjectForm, { @@ -36,7 +36,7 @@ export default () => { confirmationPhrase, }, on: { - selectNamespace: (id) => { + selectTransferLocation: (id) => { if (targetHiddenInputId && document.getElementById(targetHiddenInputId)) { document.getElementById(targetHiddenInputId).value = id; } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index 0f5560ff628..02323e5a0c6 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -43,6 +43,11 @@ export default { required: false, default: false, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -128,7 +133,7 @@ export default { </script> <template> - <div class="block js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown"> + <div class="js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown"> <div v-gl-tooltip.left.viewport data-testid="move-collapsed" @@ -141,7 +146,7 @@ export default { <gl-dropdown ref="dropdown" :block="true" - :disabled="moveInProgress" + :disabled="moveInProgress || disabled" class="hide-collapsed" toggle-class="js-sidebar-dropdown-toggle" @shown="fetchProjects" diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index 7e735f358eb..9fbf8042784 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -62,6 +62,9 @@ export default { issuableId() { return getIdFromGraphQLId(this.issuable.id); }, + issuableIid() { + return this.issuable.iid; + }, createdInPastDay() { const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date()); return createdSecondsAgo < SECONDS_IN_DAY; @@ -193,6 +196,8 @@ export default { class="issue-check gl-mr-0" :checked="checked" :data-id="issuableId" + :data-iid="issuableIid" + :data-type="issuable.type" @input="$emit('checked-input', $event)" > <span class="gl-sr-only">{{ issuable.title }}</span> diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index bc10f84b819..0318dd22bfa 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import issuableEventHub from '~/issues/list/eventhub'; import { DEFAULT_SKELETON_COUNT, PAGE_SIZE_STORAGE_KEY } from '../constants'; import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue'; import IssuableItem from './issuable_item.vue'; @@ -266,6 +267,7 @@ export default { handleIssuableCheckedInput(issuable, value) { this.checkedIssuables[this.issuableId(issuable)].checked = value; this.$emit('update-legacy-bulk-edit'); + issuableEventHub.$emit('issuables:issuableChecked', issuable, value); }, handleAllIssuablesCheckedInput(value) { Object.keys(this.checkedIssuables).forEach((issuableId) => { diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb index f3283c88740..89e8a261288 100644 --- a/app/controllers/projects/alerting/notifications_controller.rb +++ b/app/controllers/projects/alerting/notifications_controller.rb @@ -18,8 +18,9 @@ module Projects def create token = extract_alert_manager_token(request) result = notify_service.execute(token, integration) + has_something_to_return = result.success? && result.http_status != :created - if result.success? + if has_something_to_return render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status else head result.http_status diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 418e7233e21..f9a11ddb1db 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -38,9 +38,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options_hash) unfoldable_positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user).unfoldable - diffs.unfold_diff_files(unfoldable_positions) - diffs.write_cache - options = { merge_request: @merge_request, commit: commit, @@ -63,7 +60,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic options[:allow_tree_conflicts] ] - return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) + if Feature.enabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) + return + end + + diffs.unfold_diff_files(unfoldable_positions) + diffs.write_cache + + if Feature.disabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) + return + end render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options) end diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb index c3dc17694d9..27ac64e5758 100644 --- a/app/controllers/projects/prometheus/alerts_controller.rb +++ b/app/controllers/projects/prometheus/alerts_controller.rb @@ -23,11 +23,7 @@ module Projects token = extract_alert_manager_token(request) result = notify_service.execute(token) - if result.success? - render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status - else - head result.http_status - end + head result.http_status end private @@ -37,19 +33,6 @@ module Projects .new(project, params.permit!) end - def serialize_as_json(alert_obj) - serializer.represent(alert_obj) - end - - def serializer - PrometheusAlertSerializer - .new(project: project, current_user: current_user) - end - - def alerts - alerts_finder.execute - end - def alert @alert ||= alerts_finder(metric: params[:id]).execute.first || render_404 end diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb index b4235a77867..51457d443a1 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -34,9 +34,13 @@ class LicenseTemplateFinder private + def available_licenses + Licensee::License.all(featured: popular_only?) + end + def vendored_licenses strong_memoize(:vendored_licenses) do - Licensee::License.all(featured: popular_only?).map do |license| + available_licenses.map do |license| LicenseTemplate.new( key: license.key, name: license.name, diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 8cc600fc68e..c98cfed7493 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -100,8 +100,7 @@ module Types field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, null: true, calls_gitaly: true, - description: 'Detailed merge status of the merge request.', - alpha: { milestone: '15.3' } + description: 'Detailed merge status of the merge request.' field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true, calls_gitaly: true, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 361b1a8dca9..a1bef44a815 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -636,6 +636,10 @@ class ApplicationSetting < ApplicationRecord addressable_url: { allow_localhost: true, allow_local_network: false }, allow_blank: true + validates :product_analytics_enabled, + presence: true, + allow_blank: true + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb index a1defb2594f..fb127de2bc7 100644 --- a/app/models/diff_viewer/server_side.rb +++ b/app/models/diff_viewer/server_side.rb @@ -9,14 +9,6 @@ module DiffViewer self.size_limit = 5.megabytes end - def prepare! - return if Feature.enabled?(:disable_load_entire_blob_for_diff_viewer, diff_file.repository.project) - - # TODO: remove this after resolving #342703 - diff_file.old_blob&.load_all_data! - diff_file.new_blob&.load_all_data! - end - def render_error # Files that are not stored in the repository, like LFS files and # build artifacts, can only be rendered using a client-side viewer, diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 2300ec2996d..3710abc46ea 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -1,50 +1,18 @@ # frozen_string_literal: true class Experiment < ApplicationRecord - has_many :experiment_users has_many :experiment_subjects, inverse_of: :experiment validates :name, presence: true, uniqueness: true, length: { maximum: 255 } - def self.add_user(name, group_type, user, context = {}) - by_name(name).record_user_and_group(user, group_type, context) - end - - def self.add_group(name, variant:, group:) - add_subject(name, variant: variant, subject: group) - end - def self.add_subject(name, variant:, subject:) by_name(name).record_subject_and_variant!(subject, variant) end - def self.record_conversion_event(name, user, context = {}) - by_name(name).record_conversion_event_for_user(user, context) - end - def self.by_name(name) find_or_create_by!(name: name) end - # Create or update the recorded experiment_user row for the user in this experiment. - def record_user_and_group(user, group_type, context = {}) - experiment_user = experiment_users.find_or_initialize_by(user: user) - experiment_user.assign_attributes(group_type: group_type, context: merged_context(experiment_user, context)) - # We only call save when necessary because this causes the request to stick to the primary DB - # even when the save is a no-op - # https://gitlab.com/gitlab-org/gitlab/-/issues/324649 - experiment_user.save! if experiment_user.changed? - - experiment_user - end - - def record_conversion_event_for_user(user, context = {}) - experiment_user = experiment_users.find_by(user: user) - return unless experiment_user - - experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context)) - end - def record_conversion_event_for_subject(subject, context = {}) raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject) diff --git a/app/models/experiment_user.rb b/app/models/experiment_user.rb deleted file mode 100644 index e447becc1bd..00000000000 --- a/app/models/experiment_user.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class ExperimentUser < ApplicationRecord - include ::Gitlab::Experimentation::GroupTypes - - belongs_to :experiment - belongs_to :user - - enum group_type: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 } - - validates :experiment_id, presence: true - validates :user_id, presence: true - validates :group_type, presence: true -end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index ab0fdbd777f..d4867600853 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -91,7 +91,7 @@ module Integrations with_options if: :activated? do validates :api_key, presence: true, format: { with: /\A\w+\z/ } - validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true } + validates :datadog_site, format: { with: %r{\A\w+([-.]\w+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?\z}, allow_blank: true } validates :api_url, public_url: { allow_blank: true } validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? } validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? } diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index 29e1ba88528..62e2e75db2a 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -14,6 +14,10 @@ module Ml default_value_for(:iid) { SecureRandom.uuid } + def artifact_root + "/ml_candidate_#{iid}/-/" + end + class << self def with_project_id_and_iid(project_id, iid) return unless project_id.present? && iid.present? diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb index f5b944e6df5..7fa62e0ce8a 100644 --- a/app/services/bulk_imports/create_pipeline_trackers_service.rb +++ b/app/services/bulk_imports/create_pipeline_trackers_service.rb @@ -55,6 +55,8 @@ module BulkImports message: 'Pipeline skipped as source instance version not compatible with pipeline', bulk_import_entity_id: entity.id, bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline[:pipeline], minimum_source_version: minimum_version, maximum_source_version: maximum_version, diff --git a/app/services/concerns/alert_management/responses.rb b/app/services/concerns/alert_management/responses.rb index 183a831a00a..e48d07d26c0 100644 --- a/app/services/concerns/alert_management/responses.rb +++ b/app/services/concerns/alert_management/responses.rb @@ -7,6 +7,10 @@ module AlertManagement ServiceResponse.success(payload: { alerts: Array(alerts) }) end + def created + ServiceResponse.success(http_status: :created) + end + def bad_request ServiceResponse.error(message: 'Bad Request', http_status: :bad_request) end diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index 9f260345937..1e084c0e5eb 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -36,9 +36,9 @@ module Projects truncate_alerts! if max_alerts_exceeded? - alert_responses = process_prometheus_alerts + process_prometheus_alerts - alert_response(alert_responses) + created end def self.processable?(payload) @@ -152,12 +152,6 @@ module Projects .execute end end - - def alert_response(alert_responses) - alerts = alert_responses.flat_map { |resp| resp.payload[:alerts] }.compact - - success(alerts) - end end end end diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml index e3aa2d8afc9..108d340ec4d 100644 --- a/app/views/projects/_transfer.html.haml +++ b/app/views/projects/_transfer.html.haml @@ -20,5 +20,4 @@ %li= _('You will need to update your local repositories to point to the new location.') %li= _('Project visibility level will be changed to match namespace rules when transferring to a group.') = hidden_field_tag(hidden_input_id) - = label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold' .js-transfer-project-form{ data: initial_data } diff --git a/app/views/projects/protected_tags/shared/_dropdown.html.haml b/app/views/projects/protected_tags/shared/_dropdown.html.haml index 9c7f532fa29..9d5d649bc40 100644 --- a/app/views/projects/protected_tags/shared/_dropdown.html.haml +++ b/app/views/projects/protected_tags/shared/_dropdown.html.haml @@ -1,8 +1,8 @@ = f.hidden_field(:name) -= dropdown_tag('Select tag or create wildcard', += dropdown_tag(s_('ProtectedBranch|Select tag or create wildcard'), options: { toggle_class: 'js-protected-tag-select js-filter-submit wide monospace', - filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tags", + filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: s_("ProtectedBranch|Search protected tags"), footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, selected: params[:protected_tag_name], @@ -10,6 +10,6 @@ %ul.dropdown-footer-list %li - %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: "New Protected Tag" } - Create wildcard + %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: s_("ProtectedBranch|New Protected Tag") } + = s_('ProtectedBranch|Create wildcard') %code diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml index 5f3ea281278..0a85a353e27 100644 --- a/app/views/projects/protected_tags/shared/_tags_list.html.haml +++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml @@ -1,9 +1,9 @@ .protected-tags-list.js-protected-tags-list - if @protected_tags.empty? .card-header - Protected tags (0) + = s_('ProtectedBranch|Protected tags (%{tags_count})') % { tags_count: 0 } %p.settings-message.text-center - No tags are protected. + = s_('ProtectedBranch|No tags are protected.') - else - can_admin_project = can?(current_user, :admin_project, @project) @@ -16,9 +16,12 @@ %col %thead %tr - %th Protected tags (#{@protected_tags_count}) - %th Last commit - %th Allowed to create + %th + = s_('ProtectedBranch|Protected tags (%{tags_count})') % { tags_count: @protected_tags_count } + %th + = s_('ProtectedBranch|Last commit') + %th + = s_('ProtectedBranch|Allowed to create') - if can_admin_project %th %tbody diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index e6bdefc64d2..beb5b527669 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -1,5 +1,6 @@ - type = local_assigns.fetch(:type) - is_issue = type == :issues +- move_data = { projects_fetch_path: autocomplete_projects_path(project_id: @project.id), project_full_path: @project.full_path } %aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? }, 'aria-label': _('Bulk update') } .issuable-sidebar.hidden @@ -42,6 +43,9 @@ .title = _('Subscriptions') .js-subscriptions-dropdown + - if is_issue + .block + .js-move-issues{ data: move_data } = hidden_field_tag "update[issuable_ids]", [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb index ada3210624c..d23d57c33ab 100644 --- a/app/workers/bulk_imports/entity_worker.rb +++ b/app/workers/bulk_imports/entity_worker.rb @@ -12,13 +12,18 @@ module BulkImports worker_has_external_dependencies! def perform(entity_id, current_stage = nil) + @entity = ::BulkImports::Entity.find(entity_id) + if stage_running?(entity_id, current_stage) logger.info( structured_payload( bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, current_stage: current_stage, message: 'Stage running', + source_version: source_version, importer: 'gitlab_migration' ) ) @@ -29,9 +34,12 @@ module BulkImports logger.info( structured_payload( bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, current_stage: current_stage, message: 'Stage starting', + source_version: source_version, importer: 'gitlab_migration' ) ) @@ -44,23 +52,34 @@ module BulkImports ) end rescue StandardError => e - logger.error( - structured_payload( + log_exception(e, + { bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, current_stage: current_stage, - message: e.message, + message: 'Entity failed', + source_version: source_version, importer: 'gitlab_migration' - ) + } ) Gitlab::ErrorTracking.track_exception( - e, bulk_import_entity_id: entity_id, bulk_import_id: bulk_import_id(entity_id), importer: 'gitlab_migration' + e, + bulk_import_entity_id: entity_id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: source_version, + importer: 'gitlab_migration' ) end private + attr_reader :entity + def stage_running?(entity_id, stage) return unless stage @@ -71,12 +90,18 @@ module BulkImports BulkImports::Tracker.next_pipeline_trackers_for(entity_id).update(status_event: 'enqueue') end - def bulk_import_id(entity_id) - @bulk_import_id ||= Entity.find(entity_id).bulk_import_id + def source_version + entity.bulk_import.source_version_info.to_s end def logger @logger ||= Gitlab::Import::Logger.build end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + + logger.error(structured_payload(payload)) + end end end diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb index a57071ddcf1..1a5f6250429 100644 --- a/app/workers/bulk_imports/export_request_worker.rb +++ b/app/workers/bulk_imports/export_request_worker.rb @@ -22,7 +22,19 @@ module BulkImports if e.retriable?(entity) retry_request(e, entity) else - log_export_failure(e, entity) + log_exception(e, + { + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + message: "Request to export #{entity.source_type} failed", + source_version: entity.bulk_import.source_version_info.to_s, + importer: 'gitlab_migration' + } + ) + + BulkImports::Failure.create(failure_attributes(e, entity)) entity.fail_op! end @@ -41,22 +53,7 @@ module BulkImports ) end - def log_export_failure(exception, entity) - Gitlab::Import::Logger.error( - structured_payload( - log_attributes(exception, entity).merge( - bulk_import_id: entity.bulk_import_id, - bulk_import_entity_type: entity.source_type, - message: "Request to export #{entity.source_type} failed", - importer: 'gitlab_migration' - ) - ) - ) - - BulkImports::Failure.create(log_attributes(exception, entity)) - end - - def log_attributes(exception, entity) + def failure_attributes(exception, entity) { bulk_import_entity_id: entity.id, pipeline_class: 'ExportRequestWorker', @@ -84,15 +81,16 @@ module BulkImports ::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id rescue StandardError => e - Gitlab::Import::Logger.error( - structured_payload( - log_attributes(e, entity).merge( - message: 'Failed to fetch source entity id', - bulk_import_id: entity.bulk_import_id, - bulk_import_entity_type: entity.source_type, - importer: 'gitlab_migration' - ) - ) + log_exception(e, + { + message: 'Failed to fetch source entity id', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: entity.bulk_import.source_version_info.to_s, + importer: 'gitlab_migration' + } ) nil @@ -107,18 +105,29 @@ module BulkImports end def retry_request(exception, entity) - Gitlab::Import::Logger.error( - structured_payload( - log_attributes(exception, entity).merge( - message: 'Retrying export request', - bulk_import_id: entity.bulk_import_id, - bulk_import_entity_type: entity.source_type, - importer: 'gitlab_migration' - ) - ) + log_exception(exception, + { + message: 'Retrying export request', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: entity.bulk_import.source_version_info.to_s, + importer: 'gitlab_migration' + } ) self.class.perform_in(2.seconds, entity.id) end + + def logger + @logger ||= Gitlab::Import::Logger.build + end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + + logger.error(structured_payload(payload)) + end end end diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb index 6d314774cff..5716f6e3f31 100644 --- a/app/workers/bulk_imports/pipeline_worker.rb +++ b/app/workers/bulk_imports/pipeline_worker.rb @@ -17,24 +17,34 @@ module BulkImports .find_by_id(pipeline_tracker_id) if pipeline_tracker.present? + @entity = @pipeline_tracker.entity + logger.info( structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, message: 'Pipeline starting', + source_version: source_version, importer: 'gitlab_migration' ) ) run else + @entity = ::BulkImports::Entity.find(entity_id) + logger.error( structured_payload( bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_tracker_id: pipeline_tracker_id, message: 'Unstarted pipeline not found', + source_version: source_version, importer: 'gitlab_migration' ) ) @@ -46,10 +56,10 @@ module BulkImports private - attr_reader :pipeline_tracker + attr_reader :pipeline_tracker, :entity def run - return skip_tracker if pipeline_tracker.entity.failed? + return skip_tracker if entity.failed? raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout? raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed? @@ -65,33 +75,39 @@ module BulkImports fail_tracker(e) end - def bulk_import_id(entity_id) - @bulk_import_id ||= Entity.find(entity_id).bulk_import_id + def source_version + entity.bulk_import.source_version_info.to_s end def fail_tracker(exception) pipeline_tracker.update!(status_event: 'fail_op', jid: jid) - logger.error( - structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + log_exception(exception, + { + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, - message: exception.message, + message: 'Pipeline failed', + source_version: source_version, importer: 'gitlab_migration' - ) + } ) Gitlab::ErrorTracking.track_exception( exception, - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, + source_version: source_version, importer: 'gitlab_migration' ) BulkImports::Failure.create( - bulk_import_entity_id: context.entity.id, + bulk_import_entity_id: entity.id, pipeline_class: pipeline_tracker.pipeline_name, pipeline_step: 'pipeline_worker_run', exception_class: exception.class.to_s, @@ -109,7 +125,7 @@ module BulkImports delay, pipeline_tracker.id, pipeline_tracker.stage, - pipeline_tracker.entity.id + entity.id ) end @@ -128,7 +144,7 @@ module BulkImports def job_timeout? return false unless file_extraction_pipeline? - (Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT + (Time.zone.now - entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT end def export_failed? @@ -150,14 +166,17 @@ module BulkImports end def retry_tracker(exception) - logger.error( - structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + log_exception(exception, + { + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, - message: "Retrying error: #{exception.message}", + message: "Retrying pipeline", + source_version: source_version, importer: 'gitlab_migration' - ) + } ) pipeline_tracker.update!(status_event: 'retry', jid: jid) @@ -168,15 +187,23 @@ module BulkImports def skip_tracker logger.info( structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, message: 'Skipping pipeline due to failed entity', + source_version: source_version, importer: 'gitlab_migration' ) ) pipeline_tracker.update!(status_event: 'skip', jid: jid) end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + logger.error(structured_payload(payload)) + end end end |