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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 15:10:43 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 15:10:43 +0300
commitbf18f3295b550c564086efd0a32d9a25435ce216 (patch)
tree9ea92eefd45aa38a15152fb28c24d526c1525a5f /app
parent3f96425b0b9f0b4885b70db01dcd76b311ea87ab (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue233
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql5
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue171
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js31
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js7
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue1
-rw-r--r--app/assets/javascripts/issues/list/graphql.js25
-rw-r--r--app/assets/javascripts/issues/list/index.js25
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue1
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue13
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue4
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql17
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue160
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue9
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue2
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb14
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb19
-rw-r--r--app/finders/license_template_finder.rb6
-rw-r--r--app/graphql/types/merge_request_type.rb3
-rw-r--r--app/models/application_setting.rb4
-rw-r--r--app/models/diff_viewer/server_side.rb8
-rw-r--r--app/models/experiment.rb32
-rw-r--r--app/models/experiment_user.rb14
-rw-r--r--app/models/integrations/datadog.rb2
-rw-r--r--app/models/ml/candidate.rb4
-rw-r--r--app/services/bulk_imports/create_pipeline_trackers_service.rb2
-rw-r--r--app/services/concerns/alert_management/responses.rb4
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb10
-rw-r--r--app/views/projects/_transfer.html.haml1
-rw-r--r--app/views/projects/protected_tags/shared/_dropdown.html.haml8
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml13
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml4
-rw-r--r--app/workers/bulk_imports/entity_worker.rb45
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb79
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb79
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