Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
af69e63b66
commit
d2d913b606
|
|
@ -1179,14 +1179,6 @@ Layout/ArgumentAlignment:
|
|||
- 'ee/spec/elastic/migrate/20220119120500_populate_commit_permissions_in_main_index_spec.rb'
|
||||
- 'ee/spec/elastic/migrate/20221124090600_add_namespace_ancestry_ids_to_original_index_mapping_spec.rb'
|
||||
- 'ee/spec/elastic/migrate/20221221110300_backfill_traversal_ids_to_blobs_and_wiki_blobs_spec.rb'
|
||||
- 'ee/spec/factories/epic_tree_nodes.rb'
|
||||
- 'ee/spec/factories/groups.rb'
|
||||
- 'ee/spec/factories/import_states.rb'
|
||||
- 'ee/spec/factories/merge_requests.rb'
|
||||
- 'ee/spec/factories/namespaces.rb'
|
||||
- 'ee/spec/factories/projects.rb'
|
||||
- 'ee/spec/factories/security_scans.rb'
|
||||
- 'ee/spec/factories/vulnerabilities/findings.rb'
|
||||
- 'ee/spec/features/account_recovery_regular_check_spec.rb'
|
||||
- 'ee/spec/features/admin/admin_emails_spec.rb'
|
||||
- 'ee/spec/features/admin/admin_settings_spec.rb'
|
||||
|
|
@ -1902,12 +1894,6 @@ Layout/ArgumentAlignment:
|
|||
- 'spec/components/previews/pajamas/alert_component_preview.rb'
|
||||
- 'spec/components/previews/pajamas/banner_component_preview.rb'
|
||||
- 'spec/components/previews/pajamas/button_component_preview.rb'
|
||||
- 'spec/factories/ci/processable.rb'
|
||||
- 'spec/factories/draft_note.rb'
|
||||
- 'spec/factories/environments.rb'
|
||||
- 'spec/factories/group_members.rb'
|
||||
- 'spec/factories/ml/candidates.rb'
|
||||
- 'spec/factories/notes.rb'
|
||||
- 'spec/features/admin/admin_mode/login_spec.rb'
|
||||
- 'spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb'
|
||||
- 'spec/features/atom/issues_spec.rb'
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -205,7 +205,7 @@ gem 'diffy', '~> 3.4'
|
|||
gem 'diff_match_patch', '~> 0.1.0'
|
||||
|
||||
# Application server
|
||||
gem 'rack', '~> 2.2.6', '>= 2.2.6.4'
|
||||
gem 'rack', '~> 2.2.7'
|
||||
# https://github.com/zombocom/rack-timeout/blob/master/README.md#rails-apps-manually
|
||||
gem 'rack-timeout', '~> 0.6.3', require: 'rack/timeout/base'
|
||||
|
||||
|
|
|
|||
|
|
@ -463,7 +463,7 @@
|
|||
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
|
||||
{"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},
|
||||
{"name":"racc","version":"1.6.2","platform":"ruby","checksum":"58d26b3666382396fea84d33dc0639b7ee8d704156a52f8f22681f07b2f94f26"},
|
||||
{"name":"rack","version":"2.2.6.4","platform":"ruby","checksum":"d3d92be402b5881058caccc0975e6d67a1e0ba929d1d144a43daf689169bfce1"},
|
||||
{"name":"rack","version":"2.2.7","platform":"ruby","checksum":"b3377e8b2227b8ffa6b617ef8649ffb5e265e46ca8fa1f31244c809fe609829b"},
|
||||
{"name":"rack-accept","version":"0.4.5","platform":"ruby","checksum":"66247b5449db64ebb93ae2ec4af4764b87d1ae8a7463c7c68893ac13fa8d4da2"},
|
||||
{"name":"rack-attack","version":"6.6.1","platform":"ruby","checksum":"187e5d248c6a162ed8cafa8241a7b5947d9b9cf122a4870eb1cdd0db861f3a11"},
|
||||
{"name":"rack-cors","version":"1.1.1","platform":"ruby","checksum":"4702644ac6d63ebbddff372a3cd4cd573513287e3524b5a5415f678970057a4b"},
|
||||
|
|
@ -563,7 +563,6 @@
|
|||
{"name":"sentry-ruby","version":"5.8.0","platform":"ruby","checksum":"caeb121433be379fb94e991a45265a287b13a9a9083e7264f539752369d37110"},
|
||||
{"name":"sentry-sidekiq","version":"5.8.0","platform":"ruby","checksum":"90d1123d16a9fc5fd99dbad190b766dd189eaf9e2baddad641f1334e1877c779"},
|
||||
{"name":"set","version":"1.0.1","platform":"ruby","checksum":"d169fe8df4738e9da1118199429a9cf1ce0ac5e8a3cacc481e2ed24d585419dd"},
|
||||
{"name":"settingslogic","version":"2.0.9","platform":"ruby","checksum":"5925a91d0d48dfb59a6e48ae2bb9c9b801fe6fab25a8e8d302ce8699d92f2ae6"},
|
||||
{"name":"sexp_processor","version":"4.16.1","platform":"ruby","checksum":"5caadbf4bbe5ab539cb892a5bcf74ca33a2f2a897cecafdee4a63be79b4819dc"},
|
||||
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
|
||||
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
|
||||
|
|
|
|||
|
|
@ -1174,7 +1174,7 @@ GEM
|
|||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.2)
|
||||
rack (2.2.6.4)
|
||||
rack (2.2.7)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.6.1)
|
||||
|
|
@ -1871,7 +1871,7 @@ DEPENDENCIES
|
|||
pry-shell (~> 0.6.1)
|
||||
puma (~> 5.6.5)
|
||||
puma_worker_killer (~> 0.3.1)
|
||||
rack (~> 2.2.6, >= 2.2.6.4)
|
||||
rack (~> 2.2.7)
|
||||
rack-attack (~> 6.6.1)
|
||||
rack-cors (~> 1.1.1)
|
||||
rack-oauth2 (~> 1.21.3)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import FormattedStageCount from '~/analytics/cycle_analytics/components/formatted_stage_count.vue';
|
||||
import { __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
import {
|
||||
NOT_ENOUGH_DATA_ERROR,
|
||||
FIELD_KEY_TITLE,
|
||||
|
|
@ -171,6 +172,7 @@ export default {
|
|||
const { sort, direction } = this.pagination;
|
||||
this.track('click_button', { label: 'pagination' });
|
||||
this.$emit('handleUpdatePagination', { sort, direction, page });
|
||||
this.scrollToTop();
|
||||
},
|
||||
onSort({ sortBy, sortDesc }) {
|
||||
const direction = sortDesc ? PAGINATION_SORT_DIRECTION_DESC : PAGINATION_SORT_DIRECTION_ASC;
|
||||
|
|
@ -179,11 +181,14 @@ export default {
|
|||
this.$emit('handleUpdatePagination', { sort: sortBy, direction });
|
||||
this.track('click_button', { label: `sort_${sortBy}_${direction}` });
|
||||
},
|
||||
scrollToTop() {
|
||||
scrollToElement(this.$el);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div data-testid="vsa-stage-table">
|
||||
<div data-testid="vsa-stage-table" :class="{ 'gl-min-h-100vh': isLoading || !isEmptyStage }">
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-4" size="lg" />
|
||||
<gl-empty-state
|
||||
v-else-if="isEmptyStage"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlToast } from '@gitlab/ui';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import CiAdminVariables from './components/ci_admin_variables.vue';
|
||||
|
|
@ -40,6 +41,7 @@ const mountCiVariableListApp = (containerEl) => {
|
|||
component = CiProjectVariables;
|
||||
}
|
||||
|
||||
Vue.use(GlToast);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
// If the feature flag `ci_variables_pages` is enabled,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<li class="report-block-list-issue align-items-center" data-qa-selector="report_item_row">
|
||||
<li class="report-block-list-issue gl-p-3!" data-qa-selector="report_item_row">
|
||||
<component
|
||||
:is="iconComponent"
|
||||
v-if="showReportSectionStatusIcon"
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { GlIcon, GlLink, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab
|
|||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import IssueDueDate from '~/boards/components/issue_due_date.vue';
|
||||
import { TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isMetaKey } from '~/lib/utils/common_utils';
|
||||
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
import { sprintf } from '~/locale';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import relatedIssuableMixin from '../mixins/related_issuable_mixin';
|
||||
import IssueAssignees from './issue_assignees.vue';
|
||||
import IssueMilestone from './issue_milestone.vue';
|
||||
|
|
@ -26,12 +27,18 @@ export default {
|
|||
IssueDueDate,
|
||||
GlButton,
|
||||
WorkItemDetailModal,
|
||||
AbuseCategorySelector,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml,
|
||||
},
|
||||
mixins: [relatedIssuableMixin],
|
||||
inject: {
|
||||
reportAbusePath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
canReorder: {
|
||||
type: Boolean,
|
||||
|
|
@ -54,6 +61,13 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isReportDrawerOpen: false,
|
||||
reportedUserId: 0,
|
||||
reportedUrl: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stateTitle() {
|
||||
return sprintf(
|
||||
|
|
@ -92,6 +106,14 @@ export default {
|
|||
replace: true,
|
||||
});
|
||||
},
|
||||
toggleReportAbuseDrawer(isOpen, reply = {}) {
|
||||
this.isReportDrawerOpen = isOpen;
|
||||
this.reportedUrl = reply.url;
|
||||
this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0;
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
this.toggleReportAbuseDrawer(true, reply);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -233,6 +255,14 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
@close="updateWorkItemIdUrlQuery"
|
||||
@workItemDeleted="handleWorkItemDeleted"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen && reportAbusePath"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="isReportDrawerOpen"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ export default {
|
|||
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
|
||||
<comment-field-layout
|
||||
:noteable-data="getNoteableData"
|
||||
:is-internal-note="discussion.internal"
|
||||
:is-internal-note="discussionNote.internal"
|
||||
>
|
||||
<markdown-editor
|
||||
ref="markdownEditor"
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p class="gl-mt-n5 gl-text-gray-500">
|
||||
<p class="gl-mt-n3 gl-text-gray-500">
|
||||
{{ s__('ForkProject|Want to organize several dependent projects under the same namespace?') }}
|
||||
<gl-link :href="newGroupPath" target="_blank">
|
||||
{{ s__('ForkProject|Create a group') }}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export default {
|
|||
:no-results-text="__('No matches found')"
|
||||
:searchable="true"
|
||||
:searching="loading"
|
||||
toggle-class="gl-flex-direction-column gl-align-items-stretch!"
|
||||
toggle-class="gl-flex-direction-column gl-align-items-stretch! gl-rounded-top-left-none! gl-rounded-bottom-left-none! gl-w-full!"
|
||||
:toggle-text="dropdownText"
|
||||
@search="searchNamespaces"
|
||||
@select="setNamespace"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ const PERSISTENT_USER_CALLOUTS = [
|
|||
'.js-geo-migrate-hashed-storage-callout',
|
||||
'.js-unlimited-members-during-trial-alert',
|
||||
'.js-branch-rules-info-callout',
|
||||
'.js-license-check-deprecation-alert',
|
||||
];
|
||||
|
||||
const initCallouts = () => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export function initRelatedIssues(issueType = TYPE_ISSUE) {
|
|||
fullPath: el.dataset.fullPath,
|
||||
hasIssueWeightsFeature: parseBoolean(el.dataset.hasIssueWeightsFeature),
|
||||
hasIterationsFeature: parseBoolean(el.dataset.hasIterationsFeature),
|
||||
reportAbusePath: el.dataset.reportAbusePath,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement(RelatedIssuesRoot, {
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ export default class MergeRequestStore {
|
|||
this.conflictsDocsPath = data.conflicts_docs_path;
|
||||
this.reviewingDocsPath = data.reviewing_and_managing_merge_requests_docs_path;
|
||||
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
|
||||
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
|
||||
this.codeCoverageCheckHelpPagePath = data.code_coverage_check_help_page_path;
|
||||
this.licenseComplianceDocsPath = data.license_compliance_docs_path;
|
||||
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
|
||||
this.mergeImmediatelyDocsPath = data.merge_immediately_docs_path;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { s__, sprintf } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
|
||||
|
||||
|
|
@ -68,3 +69,10 @@ export const AWS_EASY_BUTTONS = [
|
|||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const LEGACY_REGISTER_HELP_URL = helpPagePath(
|
||||
'architecture/blueprints/runner_tokens/index.md',
|
||||
{
|
||||
anchor: 'using-the-authentication-token-in-place-of-the-registration-token',
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@ import {
|
|||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlSkeletonLoader,
|
||||
GlResizeObserverDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import getRunnerPlatformsQuery from './graphql/get_runner_platforms.query.graphql';
|
||||
import { PLATFORM_DOCKER, PLATFORM_KUBERNETES, PLATFORM_AWS } from './constants';
|
||||
import {
|
||||
PLATFORM_DOCKER,
|
||||
PLATFORM_KUBERNETES,
|
||||
PLATFORM_AWS,
|
||||
LEGACY_REGISTER_HELP_URL,
|
||||
} from './constants';
|
||||
|
||||
import RunnerCliInstructions from './instructions/runner_cli_instructions.vue';
|
||||
import RunnerDockerInstructions from './instructions/runner_docker_instructions.vue';
|
||||
|
|
@ -30,13 +38,16 @@ export default {
|
|||
GlDropdownItem,
|
||||
GlModal,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlSkeletonLoader,
|
||||
RunnerDockerInstructions,
|
||||
},
|
||||
directives: {
|
||||
GlResizeObserver: GlResizeObserverDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
modalId: {
|
||||
type: String,
|
||||
|
|
@ -91,7 +102,7 @@ export default {
|
|||
shown: false,
|
||||
platforms: [],
|
||||
selectedPlatform: null,
|
||||
showAlert: false,
|
||||
showErrorAlert: false,
|
||||
platformsButtonGroupVertical: false,
|
||||
};
|
||||
},
|
||||
|
|
@ -111,6 +122,14 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
showDeprecationAlert() {
|
||||
return (
|
||||
// create_runner_workflow_for_admin
|
||||
this.glFeatures.createRunnerWorkflowForAdmin ||
|
||||
// create_runner_workflow_for_namespace
|
||||
this.glFeatures.createRunnerWorkflowForNamespace
|
||||
);
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
// Refocus on dom changes, after loading data
|
||||
|
|
@ -145,7 +164,7 @@ export default {
|
|||
return this.selectedPlatform.name === platform.name;
|
||||
},
|
||||
toggleAlert(state) {
|
||||
this.showAlert = state;
|
||||
this.showErrorAlert = state;
|
||||
},
|
||||
onPlatformsButtonResize() {
|
||||
if (bp.getBreakpointSize() === 'xs') {
|
||||
|
|
@ -161,7 +180,12 @@ export default {
|
|||
downloadInstallBinary: s__('Runners|Download and install binary'),
|
||||
downloadLatestBinary: s__('Runners|Download latest binary'),
|
||||
fetchError: s__('Runners|An error has occurred fetching instructions'),
|
||||
deprecationAlertTitle: s__('Runners|Support for registration tokens is deprecated'),
|
||||
deprecationAlertContent: s__(
|
||||
"Runners|In GitLab Runner 15.6, the use of registration tokens and runner parameters in the 'register' command was deprecated. They have been replaced by authentication tokens. %{linkStart}How does this impact my current registration workflow?%{linkEnd}",
|
||||
),
|
||||
},
|
||||
LEGACY_REGISTER_HELP_URL,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -174,7 +198,22 @@ export default {
|
|||
v-on="$listeners"
|
||||
@shown="onShown"
|
||||
>
|
||||
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
|
||||
<gl-alert
|
||||
v-if="showDeprecationAlert"
|
||||
:title="$options.i18n.deprecationAlertTitle"
|
||||
variant="warning"
|
||||
:dismissible="false"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.deprecationAlertContent">
|
||||
<template #link="{ content }">
|
||||
<gl-link target="_blank" :href="$options.LEGACY_REGISTER_HELP_URL"
|
||||
>{{ content }} <gl-icon name="external-link"
|
||||
/></gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
|
||||
<gl-alert v-if="showErrorAlert" variant="danger" @dismiss="toggleAlert(false)">
|
||||
{{ $options.i18n.fetchError }}
|
||||
</gl-alert>
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ export default {
|
|||
:fetch-by-iid="fetchByIid"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', note)"
|
||||
@reportAbuse="$emit('reportAbuse', note)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<timeline-entry-item
|
||||
|
|
@ -203,6 +204,7 @@ export default {
|
|||
:fetch-by-iid="fetchByIid"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', note)"
|
||||
@reportAbuse="$emit('reportAbuse', note)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<discussion-notes-replies-wrapper>
|
||||
|
|
@ -230,6 +232,7 @@ export default {
|
|||
:fetch-by-iid="fetchByIid"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', reply)"
|
||||
@reportAbuse="$emit('reportAbuse', reply)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -152,6 +152,12 @@ export default {
|
|||
isAuthorAnAssignee() {
|
||||
return Boolean(this.assignees.filter((assignee) => assignee.id === this.author.id).length);
|
||||
},
|
||||
currentUserId() {
|
||||
return window.gon.current_user_id;
|
||||
},
|
||||
canReportAbuse() {
|
||||
return getIdFromGraphQLId(this.author.id) !== this.currentUserId;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
workItem: {
|
||||
|
|
@ -322,12 +328,14 @@ export default {
|
|||
:note-id="note.id"
|
||||
:is-author-an-assignee="isAuthorAnAssignee"
|
||||
:show-assign-unassign="canSetWorkItemMetadata"
|
||||
:can-report-abuse="canReportAbuse"
|
||||
@startReplying="showReplyForm"
|
||||
@startEditing="startEditing"
|
||||
@error="($event) => $emit('error', $event)"
|
||||
@notifyCopyDone="notifyCopyDone"
|
||||
@deleteNote="$emit('deleteNote')"
|
||||
@assignUser="assignUserAction"
|
||||
@reportAbuse="$emit('reportAbuse')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default {
|
|||
copyLinkText: __('Copy link'),
|
||||
assignUserText: __('Assign to commenting user'),
|
||||
unassignUserText: __('Unassign from commenting user'),
|
||||
reportAbuseText: __('Report abuse to administrator'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
|
|
@ -61,6 +62,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
canReportAbuse: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
assignUserActionText() {
|
||||
|
|
@ -140,6 +146,13 @@ export default {
|
|||
category="tertiary"
|
||||
no-caret
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-if="canReportAbuse"
|
||||
data-testid="abuse-note-action"
|
||||
@click="$emit('reportAbuse')"
|
||||
>
|
||||
{{ $options.i18n.reportAbuseText }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item
|
||||
data-testid="copy-link-action"
|
||||
:data-clipboard-text="noteUrl"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
|
||||
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
i18n,
|
||||
|
|
@ -91,9 +92,10 @@ export default {
|
|||
WorkItemTree,
|
||||
WorkItemNotes,
|
||||
WorkItemDetailModal,
|
||||
AbuseCategorySelector,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['fullPath'],
|
||||
inject: ['fullPath', 'reportAbusePath'],
|
||||
props: {
|
||||
isModal: {
|
||||
type: Boolean,
|
||||
|
|
@ -128,6 +130,9 @@ export default {
|
|||
? convertToGraphQLId(TYPENAME_WORK_ITEM, workItemId)
|
||||
: null,
|
||||
modalWorkItemIid: getParameterByName('work_item_iid'),
|
||||
isReportDrawerOpen: false,
|
||||
reportedUrl: '',
|
||||
reportedUserId: 0,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -498,7 +503,20 @@ export default {
|
|||
this.modalWorkItemIid = modalWorkItem.iid;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
if (this.isModal) {
|
||||
this.$emit('openReportAbuse', reply);
|
||||
} else {
|
||||
this.toggleReportAbuseDrawer(true, reply);
|
||||
}
|
||||
},
|
||||
toggleReportAbuseDrawer(isOpen, reply = {}) {
|
||||
this.isReportDrawerOpen = isOpen;
|
||||
this.reportedUrl = reply.url || {};
|
||||
this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0;
|
||||
},
|
||||
},
|
||||
|
||||
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
};
|
||||
</script>
|
||||
|
|
@ -731,9 +749,11 @@ export default {
|
|||
:is-modal="isModal"
|
||||
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
|
||||
:can-set-work-item-metadata="canAssignUnassignUser"
|
||||
:report-abuse-path="reportAbusePath"
|
||||
class="gl-pt-5"
|
||||
@error="updateError = $event"
|
||||
@has-notes="updateHasNotes"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
/>
|
||||
<gl-empty-state
|
||||
v-if="error"
|
||||
|
|
@ -749,6 +769,14 @@ export default {
|
|||
:work-item-iid="modalWorkItemIid"
|
||||
:show="true"
|
||||
@close="updateUrl"
|
||||
@openReportAbuse="toggleReportAbuseDrawer(true, $event)"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="true"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -162,6 +162,10 @@ export default {
|
|||
updateHasNotes() {
|
||||
this.hasNotes = true;
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
this.hide();
|
||||
this.$emit('openReportAbuse', reply);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -193,6 +197,7 @@ export default {
|
|||
@deleteWorkItem="deleteWorkItem"
|
||||
@update-modal="updateModal"
|
||||
@has-notes="updateHasNotes"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
/>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default function initWorkItemLinks() {
|
|||
wiHasIssuableHealthStatusFeature,
|
||||
registerPath,
|
||||
signInPath,
|
||||
wiReportAbusePath,
|
||||
} = workItemLinksRoot.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
|
|
@ -37,6 +38,7 @@ export default function initWorkItemLinks() {
|
|||
hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
|
||||
registerPath,
|
||||
signInPath,
|
||||
reportAbusePath: wiReportAbusePath,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement('work-item-links', {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
|
||||
import { isMetaKey } from '~/lib/utils/common_utils';
|
||||
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
|
||||
import {
|
||||
FORM_TYPES,
|
||||
|
|
@ -37,12 +38,13 @@ export default {
|
|||
WorkItemLinkChild,
|
||||
WorkItemLinksForm,
|
||||
WorkItemDetailModal,
|
||||
AbuseCategorySelector,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['projectPath'],
|
||||
inject: ['projectPath', 'reportAbusePath'],
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
|
|
@ -105,6 +107,9 @@ export default {
|
|||
parentIssue: null,
|
||||
formType: null,
|
||||
workItem: null,
|
||||
isReportDrawerOpen: false,
|
||||
reportedUserId: 0,
|
||||
reportedUrl: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -277,6 +282,14 @@ export default {
|
|||
clearPrefetching() {
|
||||
clearTimeout(this.prefetch);
|
||||
},
|
||||
toggleReportAbuseDrawer(isOpen, reply = {}) {
|
||||
this.isReportDrawerOpen = isOpen;
|
||||
this.reportedUrl = reply.url;
|
||||
this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0;
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
this.toggleReportAbuseDrawer(true, reply);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
title: s__('WorkItem|Tasks'),
|
||||
|
|
@ -374,6 +387,14 @@ export default {
|
|||
:work-item-iid="activeChild.iid"
|
||||
@close="closeModal"
|
||||
@workItemDeleted="handleWorkItemDeleted(activeChild)"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen && reportAbusePath"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="isReportDrawerOpen"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
reportAbusePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -284,6 +288,9 @@ export default {
|
|||
updateKey() {
|
||||
this.addNoteKey = uniqueId(`work-item-add-note-${this.workItemId}`);
|
||||
},
|
||||
reportAbuse(isOpen, reply = {}) {
|
||||
this.$emit('openReportAbuse', reply);
|
||||
},
|
||||
async fetchMoreNotes() {
|
||||
this.isLoadingMore = true;
|
||||
// copied from discussions batch logic - every fetchMore call has a higher
|
||||
|
|
@ -399,6 +406,7 @@ export default {
|
|||
:assignees="assignees"
|
||||
:can-set-work-item-metadata="canSetWorkItemMetadata"
|
||||
@deleteNote="showDeleteNoteModal($event, discussion)"
|
||||
@reportAbuse="reportAbuse(true, $event)"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const initWorkItemsRoot = () => {
|
|||
hasOkrsFeature,
|
||||
hasIssuableHealthStatusFeature,
|
||||
newCommentTemplatePath,
|
||||
reportAbusePath,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
|
|
@ -37,6 +38,7 @@ export const initWorkItemsRoot = () => {
|
|||
hasIterationsFeature: parseBoolean(hasIterationsFeature),
|
||||
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
|
||||
newCommentTemplatePath,
|
||||
reportAbusePath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@
|
|||
}
|
||||
|
||||
.label-actions-list {
|
||||
font-size: 0;
|
||||
list-style: none;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.gl-min-h-100vh {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466
|
||||
.gl-font-size-inherit,
|
||||
.font-size-inherit { font-size: inherit; }
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ module SidebarsHelper
|
|||
|
||||
if current_user&.can_admin_all_resources?
|
||||
links.append(
|
||||
{ title: s_('Navigation|Admin'), link: admin_root_path, icon: 'admin' }
|
||||
{ title: s_('Navigation|Admin Area'), link: admin_root_path, icon: 'admin' }
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ module WorkItemsHelper
|
|||
issues_list_path: project_issues_path(project),
|
||||
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
|
||||
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'),
|
||||
new_comment_template_path: profile_comment_templates_path
|
||||
new_comment_template_path: profile_comment_templates_path,
|
||||
report_abuse_path: add_category_abuse_reports_path
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectBadge < Badge
|
||||
include EachBatch
|
||||
|
||||
belongs_to :project
|
||||
|
||||
validates :project, presence: true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Board < ApplicationRecord
|
||||
include EachBatch
|
||||
|
||||
RECENT_BOARDS_SIZE = 4
|
||||
|
||||
belongs_to :group
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ module BulkImports
|
|||
|
||||
event :finish do
|
||||
transition started: :finished
|
||||
transition finished: :finished
|
||||
transition failed: :failed
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ module BulkImports
|
|||
tree_relations + file_relations + self_relation - skipped_relations
|
||||
end
|
||||
|
||||
def batchable_relations
|
||||
portable_relations.select { |relation| portable_class.reflect_on_association(relation)&.collection? }
|
||||
end
|
||||
strong_memoize_attr :batchable_relations
|
||||
|
||||
def batchable_relation?(relation)
|
||||
batchable_relations.include?(relation)
|
||||
end
|
||||
|
||||
def self_relation?(relation)
|
||||
relation == SELF_RELATION
|
||||
end
|
||||
|
|
@ -55,6 +64,19 @@ module BulkImports
|
|||
.find_relations_tree(portable_class_sym, include_import_only_tree: true).deep_stringify_keys
|
||||
end
|
||||
|
||||
# Returns an export service class for the given relation.
|
||||
# @return TreeExportService if a relation is serializable and is listed in import_export.yml
|
||||
# @return FileExportService if a relation is a file (uploads, lfs objects, git repository, etc.)
|
||||
def export_service_for(relation)
|
||||
if tree_relation?(relation)
|
||||
::BulkImports::TreeExportService
|
||||
elsif file_relation?(relation)
|
||||
::BulkImports::FileExportService
|
||||
else
|
||||
raise ::BulkImports::Error, 'Unsupported export relation'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :portable
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
class ExternalPullRequest < Ci::ApplicationRecord
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include ShaAttribute
|
||||
include EachBatch
|
||||
|
||||
belongs_to :project
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Label < ApplicationRecord
|
|||
include Sortable
|
||||
include FromUnion
|
||||
include Presentable
|
||||
include EachBatch
|
||||
|
||||
cache_markdown_field :description, pipeline: :single_line
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Milestone < ApplicationRecord
|
|||
include Importable
|
||||
include IidRoutes
|
||||
include UpdatedAtFilterable
|
||||
include EachBatch
|
||||
|
||||
prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectCustomAttribute < ApplicationRecord
|
||||
include EachBatch
|
||||
|
||||
belongs_to :project
|
||||
|
||||
validates :project, :key, :value, presence: true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class ProtectedBranch < ApplicationRecord
|
|||
include ProtectedRef
|
||||
include Gitlab::SQL::Pattern
|
||||
include FromUnion
|
||||
include EachBatch
|
||||
|
||||
belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ProtectedTag < ApplicationRecord
|
||||
include ProtectedRef
|
||||
include EachBatch
|
||||
|
||||
validates :name, uniqueness: { scope: :project_id }
|
||||
validates :project, presence: true
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class Snippet < ApplicationRecord
|
|||
include AfterCommitQueue
|
||||
extend ::Gitlab::Utils::Override
|
||||
include CreatedAtFilterable
|
||||
include EachBatch
|
||||
|
||||
MAX_FILE_COUNT = 10
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
class BatchedRelationExportService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
BATCH_CACHE_KEY = 'bulk_imports/batched_relation_export/%{export_id}/%{batch_id}'
|
||||
CACHE_DURATION = 4.hours
|
||||
|
||||
def self.cache_key(export_id, batch_id)
|
||||
Kernel.format(BATCH_CACHE_KEY, export_id: export_id, batch_id: batch_id)
|
||||
end
|
||||
|
||||
def initialize(user, portable, relation, jid)
|
||||
@user = user
|
||||
@portable = portable
|
||||
@relation = relation
|
||||
@resolved_relation = portable.public_send(relation) # rubocop:disable GitlabSecurity/PublicSend
|
||||
@jid = jid
|
||||
end
|
||||
|
||||
def execute
|
||||
return finish_export! if batches_count == 0
|
||||
|
||||
start_export!
|
||||
export.batches.destroy_all # rubocop: disable Cop/DestroyAll
|
||||
enqueue_batch_exports
|
||||
rescue StandardError => e
|
||||
fail_export!(e)
|
||||
ensure
|
||||
FinishBatchedRelationExportWorker.perform_async(export.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :portable, :relation, :jid, :config, :resolved_relation
|
||||
|
||||
def export
|
||||
@export ||= portable.bulk_import_exports.find_or_create_by!(relation: relation) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def objects_count
|
||||
resolved_relation.count
|
||||
end
|
||||
|
||||
def batches_count
|
||||
objects_count.fdiv(BATCH_SIZE).ceil
|
||||
end
|
||||
|
||||
def start_export!
|
||||
update_export!('start')
|
||||
end
|
||||
|
||||
def finish_export!
|
||||
update_export!('finish')
|
||||
end
|
||||
|
||||
def update_export!(event)
|
||||
export.update!(
|
||||
status_event: event,
|
||||
total_objects_count: objects_count,
|
||||
batched: true,
|
||||
batches_count: batches_count,
|
||||
jid: jid,
|
||||
error: nil
|
||||
)
|
||||
end
|
||||
|
||||
def enqueue_batch_exports
|
||||
resolved_relation.each_batch(of: BATCH_SIZE) do |batch, batch_number|
|
||||
batch_id = find_or_create_batch(batch_number).id
|
||||
ids = batch.pluck(batch.model.primary_key) # rubocop:disable CodeReuse/ActiveRecord
|
||||
|
||||
Gitlab::Cache::Import::Caching.set_add(self.class.cache_key(export.id, batch_id), ids, timeout: CACHE_DURATION)
|
||||
|
||||
RelationBatchExportWorker.perform_async(user.id, batch_id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_batch(batch_number)
|
||||
export.batches.find_or_create_by!(batch_number: batch_number) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def fail_export!(exception)
|
||||
Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
|
||||
|
||||
export.update!(status_event: 'fail_op', error: exception.message.truncate(255))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,14 +2,20 @@
|
|||
|
||||
module BulkImports
|
||||
class ExportService
|
||||
def initialize(portable:, user:)
|
||||
# @param portable [Project|Group] A project or a group to export.
|
||||
# @param user [User] A user performing the export.
|
||||
# @param batched [Boolean] Whether to export the data in batches.
|
||||
def initialize(portable:, user:, batched: false)
|
||||
@portable = portable
|
||||
@current_user = user
|
||||
@batched = batched
|
||||
end
|
||||
|
||||
def execute
|
||||
validate_user_permissions!
|
||||
|
||||
FileTransfer.config_for(portable).portable_relations.each do |relation|
|
||||
RelationExportWorker.perform_async(current_user.id, portable.id, portable.class.name, relation)
|
||||
RelationExportWorker.perform_async(current_user.id, portable.id, portable.class.name, relation, batched)
|
||||
end
|
||||
|
||||
ServiceResponse.success
|
||||
|
|
@ -22,6 +28,13 @@ module BulkImports
|
|||
|
||||
private
|
||||
|
||||
attr_reader :portable, :current_user
|
||||
attr_reader :portable, :current_user, :batched
|
||||
|
||||
def validate_user_permissions!
|
||||
ability = "admin_#{portable.to_ability_name}"
|
||||
|
||||
current_user.can?(ability, portable) ||
|
||||
raise(::Gitlab::ImportExport::Error.permission_error(current_user, portable))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,39 +4,58 @@ module BulkImports
|
|||
class FileExportService
|
||||
include Gitlab::ImportExport::CommandLineUtil
|
||||
|
||||
def initialize(portable, export_path, relation)
|
||||
SINGLE_OBJECT_RELATIONS = [
|
||||
FileTransfer::ProjectConfig::REPOSITORY_BUNDLE_RELATION,
|
||||
FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION
|
||||
].freeze
|
||||
|
||||
def initialize(portable, export_path, relation, user)
|
||||
@portable = portable
|
||||
@export_path = export_path
|
||||
@relation = relation
|
||||
@user = user # not used anywhere in this class at the moment
|
||||
end
|
||||
|
||||
def execute
|
||||
export_service.execute
|
||||
def execute(options = {})
|
||||
export_service.execute(options)
|
||||
|
||||
archive_exported_data
|
||||
end
|
||||
|
||||
def export_batch(ids)
|
||||
execute(batch_ids: ids)
|
||||
end
|
||||
|
||||
def exported_filename
|
||||
"#{relation}.tar"
|
||||
end
|
||||
|
||||
def exported_objects_count
|
||||
case relation
|
||||
when *SINGLE_OBJECT_RELATIONS
|
||||
1
|
||||
else
|
||||
export_service.exported_objects_count
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :export_path, :portable, :relation
|
||||
|
||||
def export_service
|
||||
case relation
|
||||
when FileTransfer::BaseConfig::UPLOADS_RELATION
|
||||
UploadsExportService.new(portable, export_path)
|
||||
when FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION
|
||||
LfsObjectsExportService.new(portable, export_path)
|
||||
when FileTransfer::ProjectConfig::REPOSITORY_BUNDLE_RELATION
|
||||
RepositoryBundleExportService.new(portable.repository, export_path, relation)
|
||||
when FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION
|
||||
RepositoryBundleExportService.new(portable.design_repository, export_path, relation)
|
||||
else
|
||||
raise BulkImports::Error, 'Unsupported relation export type'
|
||||
end
|
||||
@export_service ||= case relation
|
||||
when FileTransfer::BaseConfig::UPLOADS_RELATION
|
||||
UploadsExportService.new(portable, export_path)
|
||||
when FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION
|
||||
LfsObjectsExportService.new(portable, export_path)
|
||||
when FileTransfer::ProjectConfig::REPOSITORY_BUNDLE_RELATION
|
||||
RepositoryBundleExportService.new(portable.repository, export_path, relation)
|
||||
when FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION
|
||||
RepositoryBundleExportService.new(portable.design_repository, export_path, relation)
|
||||
else
|
||||
raise BulkImports::Error, 'Unsupported relation export type'
|
||||
end
|
||||
end
|
||||
|
||||
def archive_exported_data
|
||||
|
|
|
|||
|
|
@ -6,16 +6,26 @@ module BulkImports
|
|||
|
||||
BATCH_SIZE = 100
|
||||
|
||||
attr_reader :exported_objects_count
|
||||
|
||||
def initialize(portable, export_path)
|
||||
@portable = portable
|
||||
@export_path = export_path
|
||||
@lfs_json = {}
|
||||
@exported_objects_count = 0
|
||||
end
|
||||
|
||||
def execute
|
||||
portable.lfs_objects.find_in_batches(batch_size: BATCH_SIZE) do |batch| # rubocop: disable CodeReuse/ActiveRecord
|
||||
def execute(options = {})
|
||||
relation = portable.lfs_objects
|
||||
|
||||
if options[:batch_ids]
|
||||
relation = relation.where(relation.model.primary_key => options[:batch_ids]) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
relation.find_in_batches(batch_size: BATCH_SIZE) do |batch| # rubocop: disable CodeReuse/ActiveRecord
|
||||
batch.each do |lfs_object|
|
||||
save_lfs_object(lfs_object)
|
||||
@exported_objects_count += 1
|
||||
end
|
||||
|
||||
append_lfs_json_for_batch(batch)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
class RelationBatchExportService
|
||||
include Gitlab::ImportExport::CommandLineUtil
|
||||
|
||||
def initialize(user_id, batch_id)
|
||||
@user = User.find(user_id)
|
||||
@batch = BulkImports::ExportBatch.find(batch_id)
|
||||
@config = FileTransfer.config_for(portable)
|
||||
end
|
||||
|
||||
def execute
|
||||
start_batch!
|
||||
|
||||
export_service.export_batch(relation_batch_ids)
|
||||
compress_exported_relation
|
||||
upload_compressed_file
|
||||
|
||||
finish_batch!
|
||||
rescue StandardError => e
|
||||
fail_batch!(e)
|
||||
ensure
|
||||
FileUtils.remove_entry(export_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :batch, :config
|
||||
|
||||
delegate :export_path, to: :config
|
||||
delegate :batch_number, :export, to: :batch
|
||||
delegate :portable, :relation, to: :export
|
||||
delegate :exported_filename, :exported_objects_count, to: :export_service
|
||||
|
||||
def export_service
|
||||
@export_service ||= config.export_service_for(relation).new(portable, export_path, relation, user)
|
||||
end
|
||||
|
||||
def compress_exported_relation
|
||||
gzip(dir: export_path, filename: exported_filename)
|
||||
end
|
||||
|
||||
def upload_compressed_file
|
||||
File.open(compressed_filename) { |file| batch_upload.export_file = file }
|
||||
|
||||
batch_upload.save!
|
||||
end
|
||||
|
||||
def batch_upload
|
||||
@batch_upload ||= ::BulkImports::ExportUpload.find_or_initialize_by(export_id: export.id, batch_id: batch.id) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def compressed_filename
|
||||
File.join(export_path, "#{exported_filename}.gz")
|
||||
end
|
||||
|
||||
def relation_batch_ids
|
||||
Gitlab::Cache::Import::Caching.values_from_set(cache_key).map(&:to_i)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
BulkImports::BatchedRelationExportService.cache_key(export.id, batch.id)
|
||||
end
|
||||
|
||||
def start_batch!
|
||||
batch.update!(status_event: 'start', objects_count: 0, error: nil)
|
||||
end
|
||||
|
||||
def finish_batch!
|
||||
batch.update!(status_event: 'finish', objects_count: exported_objects_count, error: nil)
|
||||
end
|
||||
|
||||
def fail_batch!(exception)
|
||||
Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
|
||||
|
||||
batch.update!(status_event: 'fail_op', error: exception.message.truncate(255))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -22,36 +22,27 @@ module BulkImports
|
|||
upload_compressed_file(export)
|
||||
end
|
||||
ensure
|
||||
FileUtils.remove_entry(config.export_path)
|
||||
FileUtils.remove_entry(export_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :portable, :relation, :jid, :config
|
||||
|
||||
def find_or_create_export!
|
||||
validate_user_permissions!
|
||||
delegate :export_path, to: :config
|
||||
|
||||
def find_or_create_export!
|
||||
export = portable.bulk_import_exports.safe_find_or_create_by!(relation: relation)
|
||||
|
||||
return export if export.finished? && export.updated_at > EXISTING_EXPORT_TTL.ago
|
||||
return export if export.finished? && export.updated_at > EXISTING_EXPORT_TTL.ago && !export.batched?
|
||||
|
||||
export.update!(status_event: 'start', jid: jid)
|
||||
start_export!(export)
|
||||
|
||||
yield export
|
||||
|
||||
export.update!(status_event: 'finish', error: nil)
|
||||
finish_export!(export)
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, portable_id: portable.id, portable_type: portable.class.name)
|
||||
|
||||
export&.update(status_event: 'fail_op', error: e.class)
|
||||
end
|
||||
|
||||
def validate_user_permissions!
|
||||
ability = "admin_#{portable.to_ability_name}"
|
||||
|
||||
user.can?(ability, portable) ||
|
||||
raise(::Gitlab::ImportExport::Error.permission_error(user, portable))
|
||||
fail_export!(export, e)
|
||||
end
|
||||
|
||||
def remove_existing_export_file!(export)
|
||||
|
|
@ -65,16 +56,16 @@ module BulkImports
|
|||
|
||||
def export_service
|
||||
@export_service ||= if config.tree_relation?(relation) || config.self_relation?(relation)
|
||||
TreeExportService.new(portable, config.export_path, relation, user)
|
||||
TreeExportService.new(portable, export_path, relation, user)
|
||||
elsif config.file_relation?(relation)
|
||||
FileExportService.new(portable, config.export_path, relation)
|
||||
FileExportService.new(portable, export_path, relation, user)
|
||||
else
|
||||
raise BulkImports::Error, 'Unsupported export relation'
|
||||
end
|
||||
end
|
||||
|
||||
def upload_compressed_file(export)
|
||||
compressed_file = File.join(config.export_path, "#{export_service.exported_filename}.gz")
|
||||
compressed_file = File.join(export_path, "#{export_service.exported_filename}.gz")
|
||||
|
||||
upload = ExportUpload.find_or_initialize_by(export_id: export.id) # rubocop: disable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
@ -84,7 +75,30 @@ module BulkImports
|
|||
end
|
||||
|
||||
def compress_exported_relation
|
||||
gzip(dir: config.export_path, filename: export_service.exported_filename)
|
||||
gzip(dir: export_path, filename: export_service.exported_filename)
|
||||
end
|
||||
|
||||
def start_export!(export)
|
||||
export.update!(
|
||||
status_event: 'start',
|
||||
jid: jid,
|
||||
batched: false,
|
||||
batches_count: 0,
|
||||
total_objects_count: 0,
|
||||
error: nil
|
||||
)
|
||||
|
||||
export.batches.destroy_all if export.batches.any? # rubocop:disable Cop/DestroyAll
|
||||
end
|
||||
|
||||
def finish_export!(export)
|
||||
export.update!(status_event: 'finish', batched: false, error: nil)
|
||||
end
|
||||
|
||||
def fail_export!(export, exception)
|
||||
Gitlab::ErrorTracking.track_exception(exception, portable_id: portable.id, portable_type: portable.class.name)
|
||||
|
||||
export&.update(status_event: 'fail_op', error: exception.class, batched: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ module BulkImports
|
|||
@export_filename = export_filename
|
||||
end
|
||||
|
||||
def execute
|
||||
def execute(_options = {})
|
||||
return unless repository_exists?
|
||||
|
||||
repository.bundle_to_disk(bundle_filepath)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
module BulkImports
|
||||
class TreeExportService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
delegate :exported_objects_count, to: :serializer
|
||||
|
||||
def initialize(portable, export_path, relation, user)
|
||||
@portable = portable
|
||||
@export_path = export_path
|
||||
|
|
@ -11,43 +15,52 @@ module BulkImports
|
|||
end
|
||||
|
||||
def execute
|
||||
return serializer.serialize_root(config.class::SELF_RELATION) if self_relation?
|
||||
if self_relation?(relation)
|
||||
serializer.serialize_root(config.class::SELF_RELATION)
|
||||
else
|
||||
serializer.serialize_relation(relation_definition)
|
||||
end
|
||||
end
|
||||
|
||||
relation_definition = config.tree_relation_definition_for(relation)
|
||||
|
||||
raise BulkImports::Error, 'Unsupported relation export type' unless relation_definition
|
||||
|
||||
serializer.serialize_relation(relation_definition)
|
||||
def export_batch(ids)
|
||||
serializer.serialize_relation(relation_definition, batch_ids: Array.wrap(ids))
|
||||
end
|
||||
|
||||
def exported_filename
|
||||
return "#{relation}.json" if self_relation?
|
||||
|
||||
"#{relation}.ndjson"
|
||||
"#{relation}.#{extension}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
delegate :self_relation?, to: :config
|
||||
|
||||
attr_reader :export_path, :portable, :relation, :config, :user
|
||||
|
||||
# rubocop: disable CodeReuse/Serializer
|
||||
def serializer
|
||||
::Gitlab::ImportExport::Json::StreamingSerializer.new(
|
||||
@serializer ||= ::Gitlab::ImportExport::Json::StreamingSerializer.new(
|
||||
portable,
|
||||
config.portable_tree,
|
||||
json_writer,
|
||||
::Gitlab::ImportExport::Json::NdjsonWriter.new(export_path),
|
||||
exportable_path: '',
|
||||
current_user: user
|
||||
)
|
||||
end
|
||||
# rubocop: enable CodeReuse/Serializer
|
||||
|
||||
def json_writer
|
||||
::Gitlab::ImportExport::Json::NdjsonWriter.new(export_path)
|
||||
def extension
|
||||
return 'json' if self_relation?(relation)
|
||||
|
||||
'ndjson'
|
||||
end
|
||||
|
||||
def self_relation?
|
||||
relation == config.class::SELF_RELATION
|
||||
def relation_definition
|
||||
definition = config.tree_relation_definition_for(relation)
|
||||
|
||||
raise BulkImports::Error, 'Unsupported relation export type' unless definition
|
||||
|
||||
definition
|
||||
end
|
||||
strong_memoize_attr :relation_definition
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,13 +7,22 @@ module BulkImports
|
|||
BATCH_SIZE = 100
|
||||
AVATAR_PATH = 'avatar'
|
||||
|
||||
attr_reader :exported_objects_count
|
||||
|
||||
def initialize(portable, export_path)
|
||||
@portable = portable
|
||||
@export_path = export_path
|
||||
@exported_objects_count = 0
|
||||
end
|
||||
|
||||
def execute
|
||||
portable.uploads.find_each(batch_size: BATCH_SIZE) do |upload| # rubocop: disable CodeReuse/ActiveRecord
|
||||
def execute(options = {})
|
||||
relation = portable.uploads
|
||||
|
||||
if options[:batch_ids]
|
||||
relation = relation.where(relation.model.primary_key => options[:batch_ids]) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
relation.find_each(batch_size: BATCH_SIZE) do |upload| # rubocop: disable CodeReuse/ActiveRecord
|
||||
uploader = upload.retrieve_uploader
|
||||
|
||||
next unless upload.exist?
|
||||
|
|
@ -22,6 +31,7 @@ module BulkImports
|
|||
subdir_path = export_subdir_path(upload)
|
||||
mkdir_p(subdir_path)
|
||||
download_or_copy_upload(uploader, File.join(subdir_path, uploader.filename))
|
||||
@exported_objects_count += 1
|
||||
rescue StandardError => e
|
||||
# Do not fail entire project export if something goes wrong during file download
|
||||
# (e.g. downloaded file has filename that exceeds 255 characters).
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ module Ci
|
|||
def execute
|
||||
return unless pipeline.needs_processing?
|
||||
|
||||
# Run the process only if we can obtain an exclusive lease; returns nil if lease is unavailable
|
||||
success = try_obtain_lease { process! }
|
||||
|
||||
# re-schedule if we need further processing
|
||||
# Re-schedule if we need further processing
|
||||
if success && pipeline.needs_processing?
|
||||
PipelineProcessWorker.perform_async(pipeline.id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ module SystemNotes
|
|||
readable_date = date_key.humanize.downcase
|
||||
|
||||
if changed_date.nil?
|
||||
"removed #{readable_date}"
|
||||
"removed #{readable_date} #{changed_dates[date_key].first.to_s(:long)}"
|
||||
else
|
||||
"changed #{readable_date} to #{changed_date.to_s(:long)}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,24 +7,11 @@
|
|||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
|
||||
.page-title-holder.d-flex.align-items-center
|
||||
.page-title-holder.gl-display-flex.gl-align-items-center
|
||||
%h1.page-title.gl-font-size-h-display= _('Issues')
|
||||
|
||||
- if current_user
|
||||
.page-title-controls
|
||||
= render 'shared/new_project_item_vue_select'
|
||||
|
||||
- if ::Feature.enabled?(:vue_issues_dashboard)
|
||||
.js-issues-dashboard{ data: dashboard_issues_list_data(current_user) }
|
||||
- else
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
|
||||
.nav-controls
|
||||
= render 'shared/issuable/feed_buttons'
|
||||
|
||||
= render 'shared/issuable/search_bar', type: :issues
|
||||
|
||||
- if current_user && @no_filters_set
|
||||
= render 'shared/dashboard/no_filter_selected'
|
||||
- else
|
||||
= render 'shared/issues'
|
||||
.js-issues-dashboard{ data: dashboard_issues_list_data(current_user) }
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
= render 'shared/issuable/search_bar', type: :merge_requests, disable_target_branch: true
|
||||
|
||||
- if current_user && @no_filters_set
|
||||
= render 'shared/dashboard/no_filter_selected'
|
||||
= render 'no_filter_selected'
|
||||
- elsif @search_timeout_occurred
|
||||
= render 'shared/dashboard/search_timeout_occurred'
|
||||
- else
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
- content_for :flash_message do
|
||||
= dispensable_render_if_exists "projects/storage_enforcement_alert", context: @project
|
||||
= dispensable_render_if_exists "shared/namespace_storage_limit_alert", context: @project
|
||||
= dispensable_render_if_exists "projects/deprecate_license_check_alert", project: @project
|
||||
|
||||
- content_for :project_javascripts do
|
||||
- project = @target_project || @project
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@
|
|||
has_issue_weights_feature: @project.licensed_feature_available?(:issue_weights).to_s,
|
||||
help_path: help_page_path('user/project/issues/related_issues'),
|
||||
show_categorized_issues: @project.licensed_feature_available?(:blocked_issues).to_s,
|
||||
has_iterations_feature: @project.licensed_feature_available?(:iterations).to_s } }
|
||||
has_iterations_feature: @project.licensed_feature_available?(:iterations).to_s,
|
||||
report_abuse_path: add_category_abuse_reports_path } }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}';
|
||||
window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviews/index.md', anchor: 'troubleshooting')}';
|
||||
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'require-a-successful-pipeline-for-merge')}';
|
||||
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests')}';
|
||||
window.gl.mrWidgetData.code_coverage_check_help_page_path = '#{help_page_path('ci/testing/code_coverage.md', anchor: 'coverage-check-approval-rule')}';
|
||||
window.gl.mrWidgetData.security_configuration_path = '#{project_security_configuration_path(@project)}';
|
||||
window.gl.mrWidgetData.license_compliance_docs_path = '#{help_page_path('user/compliance/license_compliance/index.md', anchor: 'policies')}';
|
||||
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/approvals/rules.md', anchor: 'eligible-approvers')}';
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
= render 'shared/alerts/positioning_disabled' if @sort == 'relative_position'
|
||||
|
||||
- if @issues.to_a.any?
|
||||
%ul.content-list.issues-list.issuable-list{ class: issue_manual_ordering_class }
|
||||
= render partial: 'projects/issues/issue', collection: @issues
|
||||
= paginate @issues, theme: "gitlab"
|
||||
- else
|
||||
= render 'shared/empty_states/issues'
|
||||
|
|
@ -18,26 +18,6 @@
|
|||
= render Pajamas::ButtonComponent.new(category: :tertiary,
|
||||
icon: 'star-o',
|
||||
button_options: { class: 'add-priority has-tooltip', title: _('Prioritize'), aria_label: _('Prioritize label'), data: { placement: 'bottom' } })
|
||||
- if can?(current_user, :admin_label, label)
|
||||
%li.gl-display-inline-block
|
||||
= render Pajamas::ButtonComponent.new(href: label.edit_path, category: :tertiary, icon: 'pencil', button_options: { class: 'edit has-tooltip', 'title': _('Edit'), 'aria_label': _('Edit'), data: { placement: 'bottom' } })
|
||||
- if can?(current_user, :admin_label, label)
|
||||
%li.gl-display-inline-block
|
||||
.dropdown
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary,
|
||||
icon: 'ellipsis_v',
|
||||
button_options: { class: 'js-label-options-dropdown', 'aria_label': _('Label actions dropdown'), data: { toggle: 'dropdown' } })
|
||||
.dropdown-menu.dropdown-open-left
|
||||
%ul
|
||||
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
|
||||
%li
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
|
||||
button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do
|
||||
= _('Promote to group label')
|
||||
%li
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
|
||||
button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do
|
||||
= _('Delete')
|
||||
- if current_user
|
||||
%li.gl-display-inline-block.label-subscription.js-label-subscription.gl-ml-3
|
||||
- if label.can_subscribe_to_label_in_different_levels?
|
||||
|
|
@ -58,3 +38,23 @@
|
|||
- else
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
|
||||
= label_subscription_toggle_button_text(label, @project)
|
||||
- if can?(current_user, :admin_label, label)
|
||||
%li.gl-display-inline-block
|
||||
.dropdown
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary,
|
||||
icon: 'ellipsis_v',
|
||||
button_options: { class: 'js-label-options-dropdown gl-ml-3', 'aria_label': _('Label actions dropdown'), title: _('Label actions dropdown'), data: { toggle: 'dropdown' } })
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
%ul
|
||||
%li
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary, href: label.edit_path, variant: :link) do
|
||||
= _('Edit')
|
||||
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
|
||||
%li
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
|
||||
button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do
|
||||
= _('Promote to group label')
|
||||
%li
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
|
||||
button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do
|
||||
= _('Delete')
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
.form-group.row
|
||||
.col-12
|
||||
= f.label :description
|
||||
= f.label :description, _("Description (optional)")
|
||||
= f.text_area :description, class: "gl-form-input form-control js-quick-submit", rows: 4, data: { qa_selector: 'label_description_field' }
|
||||
.form-group.row
|
||||
.col-12
|
||||
|
|
|
|||
|
|
@ -2316,6 +2316,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: bulk_imports_finish_batched_relation_export
|
||||
:worker_name: BulkImports::FinishBatchedRelationExportWorker
|
||||
:feature_category: :importers
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: bulk_imports_pipeline
|
||||
:worker_name: BulkImports::PipelineWorker
|
||||
:feature_category: :importers
|
||||
|
|
@ -2325,6 +2334,15 @@
|
|||
:weight: 1
|
||||
:idempotent: false
|
||||
:tags: []
|
||||
- :name: bulk_imports_relation_batch_export
|
||||
:worker_name: BulkImports::RelationBatchExportWorker
|
||||
:feature_category: :importers
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: bulk_imports_relation_export
|
||||
:worker_name: BulkImports::RelationExportWorker
|
||||
:feature_category: :importers
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
class FinishBatchedRelationExportWorker
|
||||
include ApplicationWorker
|
||||
|
||||
idempotent!
|
||||
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
|
||||
feature_category :importers
|
||||
|
||||
REENQUEUE_DELAY = 5.seconds
|
||||
TIMEOUT = 6.hours
|
||||
|
||||
def perform(export_id)
|
||||
@export = Export.find_by_id(export_id)
|
||||
|
||||
return unless export
|
||||
return if export.finished? || export.failed?
|
||||
return re_enqueue if export_in_progress?
|
||||
return fail_export! if export_timeout?
|
||||
|
||||
finish_export!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :export
|
||||
|
||||
def fail_export!
|
||||
expire_cache!
|
||||
|
||||
export.batches.map(&:fail_op!)
|
||||
export.fail_op!
|
||||
end
|
||||
|
||||
def re_enqueue
|
||||
self.class.perform_in(REENQUEUE_DELAY.ago, export.id)
|
||||
end
|
||||
|
||||
def export_timeout?
|
||||
export.updated_at < TIMEOUT.ago
|
||||
end
|
||||
|
||||
def export_in_progress?
|
||||
export.batches.any?(&:started?)
|
||||
end
|
||||
|
||||
def finish_export!
|
||||
expire_cache!
|
||||
|
||||
export.finish!
|
||||
end
|
||||
|
||||
def expire_cache!
|
||||
export.batches.each do |batch|
|
||||
key = BulkImports::BatchedRelationExportService.cache_key(export.id, batch.id)
|
||||
|
||||
Gitlab::Cache::Import::Caching.expire(key, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
class RelationBatchExportWorker
|
||||
include ApplicationWorker
|
||||
|
||||
idempotent!
|
||||
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
|
||||
feature_category :importers
|
||||
sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
|
||||
|
||||
def perform(user_id, batch_id)
|
||||
RelationBatchExportService.new(user_id, batch_id).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,11 +13,16 @@ module BulkImports
|
|||
sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
|
||||
worker_resource_boundary :memory
|
||||
|
||||
def perform(user_id, portable_id, portable_class, relation)
|
||||
def perform(user_id, portable_id, portable_class, relation, batched = false)
|
||||
user = User.find(user_id)
|
||||
portable = portable(portable_id, portable_class)
|
||||
config = BulkImports::FileTransfer.config_for(portable)
|
||||
|
||||
RelationExportService.new(user, portable, relation, jid).execute
|
||||
if Gitlab::Utils.to_boolean(batched) && config.batchable_relation?(relation)
|
||||
BatchedRelationExportService.new(user, portable, relation, jid).execute
|
||||
else
|
||||
RelationExportService.new(user, portable, relation, jid).execute
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class PipelineProcessWorker
|
|||
loggable_arguments 1
|
||||
|
||||
idempotent!
|
||||
deduplicate :until_executing
|
||||
deduplicate :until_executing # Remove when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
|
||||
|
||||
def perform(pipeline_id)
|
||||
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
|
||||
|
|
@ -23,4 +23,14 @@ class PipelineProcessWorker
|
|||
.execute
|
||||
end
|
||||
end
|
||||
|
||||
# When FF `ci_pipeline_process_worker_dedup_until_executed` is removed, remove this method and
|
||||
# add `deduplicate :until_executed, if_deduplicated: :reschedule_once`, ttl: 1.minute to the class
|
||||
def self.perform_async(pipeline_id)
|
||||
return super unless Feature.enabled?(:ci_pipeline_process_worker_dedup_until_executed)
|
||||
|
||||
set(
|
||||
deduplicate: { strategy: :until_executed, options: { if_deduplicated: :reschedule_once, ttl: 1.minute } }
|
||||
).perform_async(pipeline_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_pipeline_process_worker_dedup_until_executed
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/115261
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397829
|
||||
milestone: '15.11'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: vue_issues_dashboard
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102197
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/379025
|
||||
milestone: '15.6'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: true
|
||||
|
|
@ -3,7 +3,20 @@
|
|||
return unless Gitlab.com? || Gitlab.dev_or_test_env?
|
||||
|
||||
Gitlab::Application.configure do
|
||||
if Feature.feature_flags_available? && ::Feature.enabled?(:active_record_transactions_tracking, type: :ops)
|
||||
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
|
||||
active_db_connection = begin
|
||||
ActiveRecord::Base.connection.active? # rubocop:disable Database/MultipleDatabases
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
feature_flags_available = begin
|
||||
active_db_connection && Feature::FlipperFeature.table_exists?
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
false
|
||||
end
|
||||
|
||||
if feature_flags_available && ::Feature.enabled?(:active_record_transactions_tracking, type: :ops)
|
||||
Gitlab::Database::Transaction::Observer.register!
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,8 +81,12 @@
|
|||
- 1
|
||||
- - bulk_imports_export_request
|
||||
- 1
|
||||
- - bulk_imports_finish_batched_relation_export
|
||||
- 1
|
||||
- - bulk_imports_pipeline
|
||||
- 1
|
||||
- - bulk_imports_relation_batch_export
|
||||
- 1
|
||||
- - bulk_imports_relation_export
|
||||
- 1
|
||||
- - chaos
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ You can configure custom rules for how GitLab matches Jira issue keys by definin
|
|||
- [A regex pattern](#use-regular-expression)
|
||||
- [A prefix](#use-a-prefix)
|
||||
|
||||
When you don't configure custom rules, the [default behavior](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/regex.rb#L509) is used. For more information, see the [RE2 wiki](https://github.com/google/re2/wiki/Syntax).
|
||||
When you don't configure custom rules, the [default behavior](https://gitlab.com/gitlab-org/gitlab/-/blob/710d83af298d8896f2b940faf48a46d2feb4cbaf/lib/gitlab/regex.rb#L552) is used. For more information, see the [RE2 wiki](https://github.com/google/re2/wiki/Syntax).
|
||||
|
||||
### Use regular expression
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
---
|
||||
stage: Plan
|
||||
group: Project Management
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Tutorial: Set up issue boards for team hand-off **(PREMIUM)**
|
||||
|
||||
<!-- vale gitlab.FutureTense = NO -->
|
||||
|
||||
This tutorial shows you how to set up [issue boards](../../user/project/issue_board.md) and [scoped labels](../../user/project/labels.md#scoped-labels) for two teams that work on issues in sequence.
|
||||
|
||||
In this example, you'll create two issue boards for the UX and Frontend teams.
|
||||
Using the following steps, you can create issue boards and workflows for more sub-teams, like Backend
|
||||
or Quality Assurance.
|
||||
To learn how we use workflow labels at GitLab, see [Product Development Flow](https://about.gitlab.com/handbook/product-development-flow).
|
||||
|
||||
To set up issue boards for multiple teams:
|
||||
|
||||
1. [Create a group](#create-a-group)
|
||||
1. [Create a project](#create-a-project)
|
||||
1. [Create labels](#create-labels)
|
||||
1. [Create team issue boards](#create-team-issue-boards)
|
||||
1. [Create issues for features](#create-issues-for-features)
|
||||
|
||||
## The goal workflow
|
||||
|
||||
After you set up everything, the two teams will be able to hand off issues from one board to another, for example, like this:
|
||||
|
||||
1. The project lead adds the `Workflow::Ready for design` and `Frontend` labels to a feature issue called **Redesign user profile page**.
|
||||
1. A product designer on the UX team:
|
||||
1. Checks the `Workflow::Ready for design` list on the **UX workflow** board and decides to work on the profile page redesign.
|
||||
|
||||
<!-- Image: UX board with lists:
|
||||
~Workflow::Ready for design,
|
||||
~Workflow::Design
|
||||
~Workflow::Ready for development -->
|
||||
|
||||
1. Assigns themselves to the issue.
|
||||
1. Drags the issue card to the `Workflow::Design` list. The previous workflow label is automatically removed.
|
||||
1. Creates the ✨new designs✨.
|
||||
1. [Adds the designs to the issue](../../user/project/issues/design_management.md).
|
||||
1. Drags the issue card to the `Workflow::Ready for development` list, which adds this label and removes any other `Workflow::` label.
|
||||
1. Unassigns themselves from the issue.
|
||||
1. A developer on the Frontend team:
|
||||
1. Checks the `Workflow::Ready for development` list on the **Frontend workflow** board and chooses an issue to work on.
|
||||
|
||||
<!-- Image: Frontend board, scoped to ~Frontend, with lists:
|
||||
~Workflow::Ready for development
|
||||
~Workflow::In development
|
||||
~Workflow::Complete -->
|
||||
|
||||
1. Assigns themselves to the issue.
|
||||
1. Drags the issue card to the `Workflow::In development` list. The previous workflow label is automatically removed.
|
||||
1. Adds the frontend code in a [merge request](../../user/project/merge_requests/index.md).
|
||||
1. Adds the `Workflow::Complete` label.
|
||||
|
||||
## Create a group
|
||||
|
||||
To prepare for when your project grows, start by creating a group.
|
||||
You use groups to manage one or more related projects at the same time.
|
||||
You add your users as members in the group, and assign them a role.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- If you're using an existing group for this tutorial, make sure you have at least the Reporter role
|
||||
for the group.
|
||||
|
||||
To create a group:
|
||||
|
||||
1. On the top bar, select **Create new... > New group**.
|
||||
1. Select **Create group**.
|
||||
1. Complete the fields. Name your group `Paperclip Software Factory`.
|
||||
1. Select **Create group**.
|
||||
|
||||
You've created an empty group. Next, you'll create a project that will store your issues and code.
|
||||
|
||||
## Create a project
|
||||
|
||||
The main code development work happens in projects and their repositories.
|
||||
A project contains your code and pipelines, but also the issues that are used for planning your
|
||||
upcoming code changes.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- If you're using an existing project for this tutorial, make sure you have at least the Reporter role
|
||||
for the project.
|
||||
|
||||
To create a blank project:
|
||||
|
||||
1. In your group, on the right of the page, select **New project**.
|
||||
1. Select **Create blank project**.
|
||||
1. Enter the project details:
|
||||
- In the **Project name** field, name your project `Paperclip Assistant`.
|
||||
1. Select **Create project**.
|
||||
|
||||
## Create labels
|
||||
|
||||
You need a team label and a set of workflow labels to show where in the development cycle an issue is.
|
||||
|
||||
You could create these labels in your `Paperclip Assistant` project, but it's better to create them
|
||||
in the `Paperclip Software Factory` group. This way, these labels will also be available in all the other
|
||||
projects you create later.
|
||||
|
||||
To create each label:
|
||||
|
||||
1. On the top bar, select **Main menu > Group** and find your **Paperclip Software Factory** group.
|
||||
1. On the left sidebar, select **Group information > Labels**.
|
||||
1. Select **New label**.
|
||||
1. In the **Title** field, enter the name of the label. Start with `Frontend`.
|
||||
1. Optional. Select a color by selecting from the available colors, or enter a hex color value for
|
||||
a specific color in the **Background color** field.
|
||||
1. Select **Create label**.
|
||||
|
||||
Repeat these steps to create all the labels you'll need:
|
||||
|
||||
- `Frontend`
|
||||
- `Workflow::Ready for design`
|
||||
- `Workflow::Design`
|
||||
- `Workflow::Ready for development`
|
||||
- `Workflow::In development`
|
||||
- `Workflow::Complete`
|
||||
|
||||
## Create team issue boards
|
||||
|
||||
Like with labels, you could create your issue boards in the **Paperclip Assistant** project,
|
||||
but it can be better to have them in the **Paperclip Software Factory** group. This way, you'll be able
|
||||
to manage issues from all the projects that you might create later in this group.
|
||||
|
||||
To create a new group issue board:
|
||||
|
||||
1. On the top bar, select **Main menu > Group** and find your **Paperclip Software Factory** group.
|
||||
1. On the left sidebar, select **Issues > Boards**.
|
||||
1. Create the UX workflow and Frontend workflow boards.
|
||||
|
||||
To create the **UX workflow** issue board:
|
||||
|
||||
1. In the upper-left corner of the issue board page, select the dropdown list with the current board name.
|
||||
1. Select **Create new board**.
|
||||
1. In the **Title field**, enter `UX workflow`.
|
||||
1. Clear the **Show the Open list** and **Show the Closed list** checkboxes.
|
||||
1. Select **Create board**. You should see an empty board.
|
||||
|
||||
<!-- Image: empty UX workflow board -->
|
||||
|
||||
1. Create a list for the `Workflow::Ready for design` label:
|
||||
1. In the upper-left corner of the issue board page, select **Create list**.
|
||||
1. In the column that appears, from the **Value** dropdown list, select the `Workflow::Ready for design` label.
|
||||
1. Select **Add to board**.
|
||||
1. Repeat the previous step for labels `Workflow::Design` and `Workflow::Ready for development`.
|
||||
|
||||
To create the **Frontend workflow** board:
|
||||
|
||||
1. In the upper-left corner of the issue board page, select the dropdown list with the current board name.
|
||||
1. Select **Create new board**.
|
||||
1. In the **Title field**, enter `Frontend workflow`.
|
||||
1. Clear the **Show the Open list** and **Show the Closed list** checkboxes.
|
||||
1. Expand **Scope**.
|
||||
1. Next to **Labels**, select **Edit** and select the `Frontend` label.
|
||||
1. Select **Create board**.
|
||||
1. Create a list for the `Workflow::Ready for development` label:
|
||||
1. In the upper-left corner of the issue board page, select **Create list**.
|
||||
1. In the column that appeared, from the **Value** dropdown list, select the `Workflow::Ready for development` label.
|
||||
1. Select **Add to board**.
|
||||
1. Repeat the previous step for labels `Workflow::In development` and `Workflow::Complete`.
|
||||
|
||||
For now, lists in both your boards should be empty. Next, you'll populate them with some issues.
|
||||
|
||||
## Create issues for features
|
||||
|
||||
To track upcoming features, enhancements, and bugs, you must create some issues.
|
||||
Issues belong in projects, but you can also create them directly from your issue board.
|
||||
|
||||
To create an issue from your board:
|
||||
|
||||
1. In the upper-left corner of the issue board page, select the dropdown list with the current board name.
|
||||
1. Select **UX workflow**.
|
||||
1. On the `Workflow::Ready for development` list, select **List actions** (**{ellipsis_v}**) **> Create new issue**.
|
||||
1. Complete the fields:
|
||||
1. Under **Title**, enter `Redesign user profile page`.
|
||||
1. Under **Projects**, select **Paperclip Software Factory / Paperclip Assistant**.
|
||||
1. Select **Create issue**. Because you created the new issue in the label list, it gets created
|
||||
with this label.
|
||||
1. Add the `Frontend` label, because only issues with this label appear on the Frontend team's board:
|
||||
1. Select the issue card (not its title), and a sidebar appears on the right.
|
||||
1. In the **Labels** section of the sidebar, select **Edit**.
|
||||
1. From the **Assign labels** dropdown list, select the `Workflow::Ready for design` and
|
||||
`Frontend` labels. The selected labels are marked with a checkmark.
|
||||
1. To apply your changes to labels, select **X** next to **Assign labels** or select any area
|
||||
outside the label section.
|
||||
|
||||
Repeat these steps to create a few more issues with the same labels.
|
||||
|
||||
You should now see at least one issue there, ready for your product designers to start working on!
|
||||
|
||||
<!-- Image: UX workflow board with at least one issue in the `Workflow::Ready for design` list -->
|
||||
|
||||
Congratulations! Now your teams can start collaborating on amazing software.
|
||||
|
||||
## Learn more about project management in GitLab
|
||||
|
||||
Find other tutorials about project management on the [tutorials page](../plan_and_track.md).
|
||||
|
|
@ -274,13 +274,12 @@ By default, the vulnerability report does not show vulnerabilities of `dismissed
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9928) in GitLab 12.2.
|
||||
> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/357300) the Vulnerability-Check feature in GitLab 15.0.
|
||||
> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/397067) the License-Check feature in GitLab 16.0.
|
||||
|
||||
You can enforce an additional approval for merge requests that would introduce one of the following
|
||||
security issues:
|
||||
|
||||
- A security vulnerability. For more details, read [Scan result policies](policies/scan-result-policies.md).
|
||||
- A software license compliance violation. For more details, read
|
||||
[Enabling license approvals within a project](../compliance/license_check_rules.md#enabling-license-approvals-within-a-project).
|
||||
|
||||
## Using private Maven repositories
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ contains more than 100 patterns.
|
|||
|
||||
Most Secret Detection patterns search for specific types of secrets.
|
||||
Many services add prefixes or other structural details to their secrets so they can be identified if they're leaked.
|
||||
For example, GitLab [adds a `glpat-` prefix](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix) to project, group, and project access tokens by default.
|
||||
For example, GitLab [adds a `glpat-` prefix](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix) to project, group, and personal access tokens by default.
|
||||
|
||||
To provide more reliable, high-confidence results, Secret Detection only looks for passwords or other unstructured secrets in specific contexts like URLs.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,84 +0,0 @@
|
|||
---
|
||||
type: reference, howto
|
||||
stage: Govern
|
||||
group: Security Policies
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# License Check Policies (deprecated) **(ULTIMATE)**
|
||||
|
||||
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/390417) in GitLab 15.9.
|
||||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/390417) in GitLab 15.9. Users should migrate over to use [License Approval Policies](license_approval_policies.md) prior to GitLab 16.0.
|
||||
|
||||
License check policies allow you to specify licenses that are `allowed` or `denied` in a project. If a `denied`
|
||||
license is newly committed it blocks the merge request and instructs the developer to remove it.
|
||||
Note, the merge request is not able to be merged until the `denied` license is removed.
|
||||
You may add a [`License-Check` approval rule](#enabling-license-approvals-within-a-project),
|
||||
which enables a designated approver that can approve and then merge a merge request with `denied` license.
|
||||
|
||||
These policies can be configured by using the [Managed Licenses API](../../api/managed_licenses.md).
|
||||
|
||||

|
||||
|
||||
The **Policies** tab in the project's license compliance section displays your project's license
|
||||
policies. Project maintainers can specify policies in this section.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Developers of the project can view the policies configured in a project.
|
||||
|
||||

|
||||
|
||||
## Enabling License Approvals within a project
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Maintainer or Owner role.
|
||||
|
||||
`License-Check` is a [merge request approval](../project/merge_requests/approvals/index.md) rule
|
||||
you can enable to allow an individual or group to approve a merge request that contains a `denied`
|
||||
license.
|
||||
|
||||
You can enable `License-Check` one of two ways:
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **Merge request approvals**.
|
||||
1. Select **Enable** or **Edit**.
|
||||
1. Add or change the **Rule name** to `License-Check` (case sensitive).
|
||||
|
||||

|
||||
|
||||
- Create an approval group in the [project policies section for License Compliance](license_check_rules.md#license-check-policies-deprecated).
|
||||
You must set this approval group's number of approvals required to greater than zero. After you
|
||||
enable this group in your project, the approval rule is enabled for all merge requests.
|
||||
|
||||
Any code changes cause the approvals required to reset.
|
||||
|
||||
An approval is required when a license report:
|
||||
|
||||
- Contains a dependency that includes a software license that is `denied`.
|
||||
- Is not generated during pipeline execution.
|
||||
|
||||
An approval is optional when a license report:
|
||||
|
||||
- Contains no software license violations.
|
||||
- Contains only new licenses that are `allowed` or unknown.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### The License Compliance widget is stuck in a loading state
|
||||
|
||||
A loading spinner is displayed in the following scenarios:
|
||||
|
||||
- While the pipeline is in progress.
|
||||
- If the pipeline is complete, but still parsing the results in the background.
|
||||
- If the license scanning job is complete, but the pipeline is still running.
|
||||
|
||||
The License Compliance widget polls every few seconds for updated results. When the pipeline is complete, the first poll after pipeline completion triggers the parsing of the results. This can take a few seconds depending on the size of the generated report.
|
||||
|
||||
The final state is when a successful pipeline run has been completed, parsed, and the licenses displayed in the widget.
|
||||
|
|
@ -25,8 +25,8 @@ For the job to activate, License Finder needs to find a compatible package defin
|
|||
GitLab checks the License Compliance report, compares the
|
||||
licenses between the source and target branches, and shows the information right on the merge
|
||||
request. Denied licenses are indicated by a `x` red icon next to them as well as new licenses that
|
||||
need a decision from you. In addition, you can [manually allow or deny](../license_check_rules.md) licenses in your
|
||||
project's license compliance policy section. If a denied license is detected in a new commit,
|
||||
need a decision from you. In addition, you can [manually allow or deny](../license_approval_policies.md) licenses in your
|
||||
project's security policies section. If a denied license is detected in a new commit,
|
||||
GitLab blocks any merge requests containing that commit and instructs the developer to remove the
|
||||
license.
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ dependencies to find their licenses.
|
|||
|
||||
GitLab has limited support for [composite licenses](https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/).
|
||||
License compliance can read multiple licenses, but always considers them combined using the `AND` operator. For example,
|
||||
if a dependency has two licenses, and one of them is allowed and the other is denied by the project [policy](../license_check_rules.md),
|
||||
if a dependency has two licenses, and one of them is allowed and the other is denied by the project [license approval policy](../license_approval_policies.md),
|
||||
GitLab evaluates the composite license as _denied_, as this is the safer option.
|
||||
The ability to support other license expression operators (like `OR`, `WITH`) is tracked
|
||||
in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/6571).
|
||||
|
|
@ -693,8 +693,8 @@ Additional configuration may be needed for connecting to private registries for:
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212388) in GitLab 13.3.
|
||||
|
||||
Prior to GitLab 13.3, offline environments required an exact name match for [project policies](../license_check_rules.md).
|
||||
In GitLab 13.3 and later, GitLab matches the name of [project policies](../license_check_rules.md)
|
||||
Prior to GitLab 13.3, offline environments required an exact name match for [project policies](../license_approval_policies.md).
|
||||
In GitLab 13.3 and later, GitLab matches the name of [project policies](../license_approval_policies.md)
|
||||
with identifiers from the [SPDX license list](https://spdx.org/licenses/).
|
||||
A local copy of the SPDX license list is distributed with the GitLab instance. If needed, the GitLab
|
||||
instance's administrator can manually update it with a [Rake task](../../../raketasks/spdx.md).
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ Create lists to order issues by topic and quickly change them between topics or
|
|||
such as between **UX**, **Frontend**, and **Backend**. The changes are reflected across boards,
|
||||
as changing lists updates the labels on each issue accordingly.
|
||||
|
||||
#### Advanced team handover
|
||||
#### Issue board workflow between teams
|
||||
|
||||
For example, suppose we have a UX team with an issue board that contains:
|
||||
|
||||
|
|
@ -167,6 +167,9 @@ When finished with something, they move the card to **Frontend**. The Frontend t
|
|||
Cards finished by the UX team automatically appear in the **Frontend** column when they are ready
|
||||
for them.
|
||||
|
||||
For a tutorial how to set up your boards in a similar way with [scoped labels](labels.md#scoped-labels), see
|
||||
[Tutorial: Set up issue boards for team hand-off](../../tutorials/boards_for_teams/index.md).
|
||||
|
||||
NOTE:
|
||||
For a broader use case, see the blog post
|
||||
[What is GitLab Flow?](https://about.gitlab.com/topics/version-control/what-is-gitlab-flow/).
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ SSH uses two keys, a public key and a private key.
|
|||
- The public key can be distributed.
|
||||
- The private key should be protected.
|
||||
|
||||
You cannot expose data by uploading your public key. When you need to copy or upload your SSH public key, make sure you do not accidentally copy or upload your private key instead.
|
||||
It is not possible to reveal confidential data by uploading your public key. When you need to copy or upload your SSH public key, make sure you do not accidentally copy or upload your private key instead.
|
||||
|
||||
You can use your private key to [sign commits](project/repository/ssh_signed_commits/index.md),
|
||||
which makes your use of GitLab and your data even more secure.
|
||||
|
|
|
|||
|
|
@ -40,19 +40,6 @@ module Feature
|
|||
class << self
|
||||
delegate :group, to: :flipper
|
||||
|
||||
def feature_flags_available?
|
||||
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
|
||||
active_db_connection = begin
|
||||
ActiveRecord::Base.connection.active? # rubocop:disable Database/MultipleDatabases
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
active_db_connection && Feature::FlipperFeature.table_exists?
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
false
|
||||
end
|
||||
|
||||
def all
|
||||
flipper.features.to_a
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ module Gitlab
|
|||
|
||||
BATCH_SIZE = 100
|
||||
|
||||
attr_reader :exported_objects_count
|
||||
|
||||
class Raw < String
|
||||
def to_json(*_args)
|
||||
to_s
|
||||
|
|
@ -21,6 +23,7 @@ module Gitlab
|
|||
@relations_schema = relations_schema
|
||||
@json_writer = json_writer
|
||||
@logger = logger
|
||||
@exported_objects_count = 0
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -40,21 +43,28 @@ module Gitlab
|
|||
relations_schema.merge(include: nil, preloads: nil, unsafe: true))
|
||||
|
||||
json_writer.write_attributes(exportable_path, attributes)
|
||||
|
||||
increment_exported_objects_counter
|
||||
end
|
||||
|
||||
def serialize_relation(definition)
|
||||
def serialize_relation(definition, options = {})
|
||||
raise ArgumentError, 'definition needs to be Hash' unless definition.is_a?(Hash)
|
||||
raise ArgumentError, 'definition needs to have exactly one Hash element' unless definition.one?
|
||||
|
||||
key, options = definition.first
|
||||
key, definition_options = definition.first
|
||||
|
||||
record = exportable.public_send(key) # rubocop: disable GitlabSecurity/PublicSend
|
||||
|
||||
if options[:batch_ids]
|
||||
record = record.where(record.model.primary_key => Array.wrap(options[:batch_ids]).map(&:to_i))
|
||||
end
|
||||
|
||||
if record.is_a?(ActiveRecord::Relation)
|
||||
serialize_many_relations(key, record, options)
|
||||
serialize_many_relations(key, record, definition_options)
|
||||
elsif record.respond_to?(:each) # this is to support `project_members` that return an Array
|
||||
serialize_many_each(key, record, options)
|
||||
serialize_many_each(key, record, definition_options)
|
||||
else
|
||||
serialize_single_relation(key, record, options)
|
||||
serialize_single_relation(key, record, definition_options)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -76,6 +86,8 @@ module Gitlab
|
|||
|
||||
items << exportable_json_record(record, options, key)
|
||||
|
||||
increment_exported_objects_counter
|
||||
|
||||
after_read_callback(record)
|
||||
end
|
||||
end
|
||||
|
|
@ -175,6 +187,8 @@ module Gitlab
|
|||
enumerator = Enumerator.new do |items|
|
||||
records.each do |record|
|
||||
items << exportable_json_record(record, options, key)
|
||||
|
||||
increment_exported_objects_counter
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -187,6 +201,8 @@ module Gitlab
|
|||
json = exportable_json_record(record, options, key)
|
||||
|
||||
json_writer.write_relation(@exportable_path, key, json)
|
||||
|
||||
increment_exported_objects_counter
|
||||
end
|
||||
|
||||
def includes
|
||||
|
|
@ -263,6 +279,10 @@ module Gitlab
|
|||
message += ". Number of records to export: #{size}" if size
|
||||
logger.info(message: message, **log_base_data)
|
||||
end
|
||||
|
||||
def increment_exported_objects_counter
|
||||
@exported_objects_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ module Gitlab
|
|||
# @return [Boolean, String, Array, Hash, Object]
|
||||
# @raise [JSON::ParserError]
|
||||
def handle_legacy_mode!(data)
|
||||
return data unless Feature.feature_flags_available?
|
||||
return data unless Feature::FlipperFeature.table_exists?
|
||||
return data unless Feature.enabled?(:json_wrapper_legacy_mode)
|
||||
|
||||
raise parser_error if INVALID_LEGACY_TYPES.any? { |type| data.is_a?(type) }
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def options
|
||||
# Remove line below when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
|
||||
return job_deduplication[:options] if job_deduplication[:options]
|
||||
return {} unless worker_klass
|
||||
return {} unless worker_klass.respond_to?(:get_deduplication_options)
|
||||
|
||||
|
|
@ -200,6 +202,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def strategy
|
||||
# Remove line below when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
|
||||
return job_deduplication[:strategy] if job_deduplication[:strategy]
|
||||
return DEFAULT_STRATEGY unless worker_klass
|
||||
return DEFAULT_STRATEGY unless worker_klass.respond_to?(:idempotent?)
|
||||
return STRATEGY_NONE unless worker_klass.deduplication_enabled?
|
||||
|
|
@ -207,6 +211,22 @@ module Gitlab
|
|||
worker_klass.get_deduplicate_strategy
|
||||
end
|
||||
|
||||
# Returns the deduplicate settings stored in the job itself; remove this method
|
||||
# when FF `ci_pipeline_process_worker_dedup_until_executed` is removed
|
||||
def job_deduplication
|
||||
return {} unless job['deduplicate']
|
||||
|
||||
# Sometimes this setting is returned with all string keys/values; we need
|
||||
# to ensure the keys and values of the hash are fully symbolized or numeric
|
||||
job['deduplicate'].deep_symbolize_keys.tap do |hash|
|
||||
hash[:strategy] = hash[:strategy]&.to_sym
|
||||
hash[:options]&.each do |k, v|
|
||||
hash[:options][k] = k == :ttl ? v.to_i : v.to_sym
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
strong_memoize_attr :job_deduplication
|
||||
|
||||
def worker_class_name
|
||||
job['class']
|
||||
end
|
||||
|
|
|
|||
|
|
@ -853,9 +853,6 @@ msgstr ""
|
|||
msgid "%{level_name} is not allowed since the fork source project has lower visibility."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{license_check_docs_link_start}License-Check%{link_end} is enabled for this project. This feature has been %{deprecation_docs_link_url}deprecated%{link_end} in GitLab 15.9 and is planned for %{removal_docs_link_url}removal%{link_end} in 16.0. You can create a %{scan_result_policy_link_start}scan result policy%{link_end} to continue enforcing your license approval requirements."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{linkStart} Learn more%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5552,9 +5549,6 @@ msgstr ""
|
|||
msgid "Approved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Approved MRs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Approved members will use an additional seat in your subscription, which may override your user cap."
|
||||
msgid_plural "Approved members will use an additional %d seats in your subscription, which may override your user cap."
|
||||
msgstr[0] ""
|
||||
|
|
@ -5898,9 +5892,6 @@ msgstr ""
|
|||
msgid "AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user."
|
||||
msgstr ""
|
||||
|
||||
msgid "Ascending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ask again later"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9565,9 +9556,6 @@ msgstr ""
|
|||
msgid "Closed (moved)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Closed MRs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Closed date"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11030,6 +11018,9 @@ msgstr ""
|
|||
msgid "ComplianceFramework|No pipeline configuration found"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceReport|Add framework"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceReport|Apply framework to selected projects"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11060,9 +11051,6 @@ msgstr ""
|
|||
msgid "ComplianceReport|Less than 2 approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceReport|No framework"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceReport|No projects found"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11783,18 +11771,9 @@ msgstr ""
|
|||
msgid "ContributionAnalytics|%{createdCount} created, %{mergedCount} merged, %{closedCount} closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{created} created, %{closed} closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{created} created, %{merged} merged, %{closed} closed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{pushCount} by %{authorCount}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|%{pushes}, more than %{commits} by %{contributors}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContributionAnalytics|Approved MRs"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11870,9 +11849,6 @@ msgstr ""
|
|||
msgid "Contributions for %{calendar_date}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contributions per group member"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contributor"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14874,9 +14850,6 @@ msgstr ""
|
|||
msgid "Deprioritize label"
|
||||
msgstr ""
|
||||
|
||||
msgid "Descending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Describe the goal of the changes and what reviewers should be aware of."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -25969,9 +25942,6 @@ msgstr ""
|
|||
msgid "License overview"
|
||||
msgstr ""
|
||||
|
||||
msgid "License-Check has been %{deprecation_docs_link_url}deprecated%{link_end} in GitLab 15.9 and is planned for %{removal_docs_link_url}removal%{link_end} in 16.0. You can create a %{scan_result_policy_link_start}scan result policy%{link_end} to continue enforcing your license approval requirements."
|
||||
msgstr ""
|
||||
|
||||
msgid "LicenseCompliance|%{docLinkStart}License Approvals%{docLinkEnd} are active"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26343,9 +26313,6 @@ msgstr ""
|
|||
msgid "Loading %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading contribution stats for group members"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading files, directories, and submodules in the path %{path} for commit reference %{ref}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27649,9 +27616,6 @@ msgstr ""
|
|||
msgid "Merged"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merged MRs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28864,7 +28828,7 @@ msgstr ""
|
|||
msgid "NavigationTheme|Red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Navigation|Admin"
|
||||
msgid "Navigation|Admin Area"
|
||||
msgstr ""
|
||||
|
||||
msgid "Navigation|Analyze"
|
||||
|
|
@ -30863,12 +30827,6 @@ msgstr ""
|
|||
msgid "Opened"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opened MRs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opened issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenedNDaysAgo|Created"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -36267,9 +36225,6 @@ msgstr ""
|
|||
msgid "PushRule|Reject unverified users"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pushed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pushes"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38278,6 +38233,9 @@ msgstr ""
|
|||
msgid "Runners|If both settings are disabled, new runners cannot be registered."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|In GitLab Runner 15.6, the use of registration tokens and runner parameters in the 'register' command was deprecated. They have been replaced by authentication tokens. %{linkStart}How does this impact my current registration workflow?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Install GitLab Runner"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39513,24 +39471,12 @@ msgstr ""
|
|||
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when the license compliance report contains a denied license."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Coverage-Check"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Learn more about Coverage-Check"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Learn more about License-Check"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|License-Check"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Requires approval for Denied licenses. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39864,9 +39810,6 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|License scanner finds any license %{matching} %{licenses}%{detection} in an open merge request targeting %{branches}."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|License-Check has been deprecated"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|New policy"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -42016,9 +41959,6 @@ msgstr ""
|
|||
msgid "Something went wrong while fetching details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching group member contributions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching latest comments."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -46609,9 +46549,6 @@ msgstr ""
|
|||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total Contributions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total Score"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.2.0",
|
||||
"@gitlab/svgs": "3.40.0",
|
||||
"@gitlab/ui": "61.1.1",
|
||||
"@gitlab/ui": "61.3.0",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@gitlab/web-ide": "0.0.1-dev-20230425040132",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ FactoryBot.define do
|
|||
upload { association(:bulk_import_export_upload) }
|
||||
|
||||
status { 0 }
|
||||
batch_number { 1 }
|
||||
|
||||
trait :started do
|
||||
status { 0 }
|
||||
|
|
|
|||
|
|
@ -20,5 +20,9 @@ FactoryBot.define do
|
|||
trait :failed do
|
||||
status { -1 }
|
||||
end
|
||||
|
||||
trait :batched do
|
||||
batched { true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,13 +26,19 @@ FactoryBot.define do
|
|||
before(:create) do |processable, evaluator|
|
||||
next if processable.ci_stage
|
||||
|
||||
if ci_stage = processable.pipeline.stages.find_by(name: evaluator.stage)
|
||||
processable.ci_stage = ci_stage
|
||||
else
|
||||
processable.ci_stage = create(:ci_stage, pipeline: processable.pipeline,
|
||||
project: processable.project || evaluator.project,
|
||||
name: evaluator.stage, position: evaluator.stage_idx, status: 'created')
|
||||
end
|
||||
processable.ci_stage =
|
||||
if ci_stage = processable.pipeline.stages.find_by(name: evaluator.stage)
|
||||
ci_stage
|
||||
else
|
||||
create(
|
||||
:ci_stage,
|
||||
pipeline: processable.pipeline,
|
||||
project: processable.project || evaluator.project,
|
||||
name: evaluator.stage,
|
||||
position: evaluator.stage_idx,
|
||||
status: 'created'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
trait :waiting_for_resource do
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
position do
|
||||
association(:image_diff_position,
|
||||
file: path,
|
||||
diff_refs: diff_refs)
|
||||
association(:image_diff_position, file: path, diff_refs: diff_refs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,20 +46,19 @@ FactoryBot.define do
|
|||
after(:create) do |environment, evaluator|
|
||||
pipeline = create(:ci_pipeline, project: environment.project)
|
||||
|
||||
deployable = create(:ci_build, :success, name: "#{environment.name}:deploy",
|
||||
pipeline: pipeline)
|
||||
deployable = create(:ci_build, :success, name: "#{environment.name}:deploy", pipeline: pipeline)
|
||||
|
||||
deployment = create(:deployment,
|
||||
:success,
|
||||
environment: environment,
|
||||
project: environment.project,
|
||||
deployable: deployable,
|
||||
ref: evaluator.ref,
|
||||
sha: environment.project.commit(evaluator.ref).id)
|
||||
deployment = create(
|
||||
:deployment,
|
||||
:success,
|
||||
environment: environment,
|
||||
project: environment.project,
|
||||
deployable: deployable,
|
||||
ref: evaluator.ref,
|
||||
sha: environment.project.commit(evaluator.ref).id
|
||||
)
|
||||
|
||||
teardown_build = create(:ci_build, :manual,
|
||||
name: "#{environment.name}:teardown",
|
||||
pipeline: pipeline)
|
||||
teardown_build = create(:ci_build, :manual, name: "#{environment.name}:teardown", pipeline: pipeline)
|
||||
|
||||
deployment.update_column(:on_stop, teardown_build.name)
|
||||
environment.update_attribute(:deployments, [deployment])
|
||||
|
|
|
|||
|
|
@ -60,10 +60,12 @@ FactoryBot.define do
|
|||
|
||||
after(:build) do |group_member, evaluator|
|
||||
if evaluator.tasks_to_be_done.present?
|
||||
build(:member_task,
|
||||
member: group_member,
|
||||
project: build(:project, namespace: group_member.source),
|
||||
tasks_to_be_done: evaluator.tasks_to_be_done)
|
||||
build(
|
||||
:member_task,
|
||||
member: group_member,
|
||||
project: build(:project, namespace: group_member.source),
|
||||
tasks_to_be_done: evaluator.tasks_to_be_done
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ FactoryBot.define do
|
|||
|
||||
trait :with_artifact do
|
||||
after(:create) do |candidate|
|
||||
candidate.package = FactoryBot.create(:generic_package,
|
||||
name: candidate.package_name,
|
||||
version: candidate.package_version,
|
||||
project: candidate.project)
|
||||
candidate.package = FactoryBot.create(
|
||||
:generic_package,
|
||||
name: candidate.package_name,
|
||||
version: candidate.package_version,
|
||||
project: candidate.project
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -55,28 +55,34 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
position do
|
||||
association(:text_diff_position,
|
||||
file: "files/ruby/popen.rb",
|
||||
old_line: nil,
|
||||
new_line: line_number,
|
||||
diff_refs: diff_refs)
|
||||
association(
|
||||
:text_diff_position,
|
||||
file: "files/ruby/popen.rb",
|
||||
old_line: nil,
|
||||
new_line: line_number,
|
||||
diff_refs: diff_refs
|
||||
)
|
||||
end
|
||||
|
||||
trait :folded_position do
|
||||
position do
|
||||
association(:text_diff_position,
|
||||
file: "files/ruby/popen.rb",
|
||||
old_line: 1,
|
||||
new_line: 1,
|
||||
diff_refs: diff_refs)
|
||||
association(
|
||||
:text_diff_position,
|
||||
file: "files/ruby/popen.rb",
|
||||
old_line: 1,
|
||||
new_line: 1,
|
||||
diff_refs: diff_refs
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
factory :image_diff_note_on_merge_request do
|
||||
position do
|
||||
association(:image_diff_position,
|
||||
file: "files/images/any_image.png",
|
||||
diff_refs: diff_refs)
|
||||
association(
|
||||
:image_diff_position,
|
||||
file: "files/images/any_image.png",
|
||||
diff_refs: diff_refs
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -101,9 +107,11 @@ FactoryBot.define do
|
|||
|
||||
factory :diff_note_on_design, parent: :note, traits: [:on_design], class: 'DiffNote' do
|
||||
position do
|
||||
association(:image_diff_position,
|
||||
file: noteable.full_path,
|
||||
diff_refs: noteable.diff_refs)
|
||||
association(
|
||||
:image_diff_position,
|
||||
file: noteable.full_path,
|
||||
diff_refs: noteable.diff_refs
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.describe 'Navigation bar counter', :use_clean_rails_memory_store_caching,
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'reflects dashboard issues count' do
|
||||
it 'reflects dashboard issues count', :js do
|
||||
visit issues_path
|
||||
|
||||
expect_counters('issues', '1', n_("%d assigned issue", "%d assigned issues", 1) % 1)
|
||||
|
|
|
|||
|
|
@ -6,43 +6,55 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
|
|||
include Features::SortingHelpers
|
||||
include FilteredSearchHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:milestone) { create(:milestone, project: project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||
|
||||
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
|
||||
let!(:issue2) { create(:issue, project: project, author: user, assignees: [user], milestone: milestone) }
|
||||
let_it_be(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
|
||||
let_it_be(:issue2) { create(:issue, project: project, author: user, assignees: [user], milestone: milestone) }
|
||||
let_it_be(:label) { create(:label, project: project, title: 'bug') }
|
||||
let_it_be(:label_link) { create(:label_link, label: label, target: issue) }
|
||||
|
||||
let_it_be(:project2) { create(:project, namespace: user.namespace) }
|
||||
let_it_be(:label2) { create(:label, title: 'bug') }
|
||||
|
||||
before do
|
||||
project.labels << label
|
||||
project2.labels << label2
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit_issues
|
||||
end
|
||||
|
||||
context 'without any filter' do
|
||||
it 'shows error message' do
|
||||
visit issues_dashboard_path
|
||||
|
||||
expect(page).to have_content 'Please select at least one filter to see results'
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by milestone' do
|
||||
it 'shows all issues with no milestone' do
|
||||
input_filtered_search("milestone:=none")
|
||||
visit issues_dashboard_path
|
||||
|
||||
select_tokens 'Milestone', '=', 'None', submit: true
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
||||
it 'shows all issues with the selected milestone' do
|
||||
input_filtered_search("milestone:=%\"#{milestone.title}\"")
|
||||
visit issues_dashboard_path
|
||||
|
||||
select_tokens 'Milestone', '=', milestone.title, submit: true
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
expect(page).to have_selector('.issue', count: 1)
|
||||
end
|
||||
|
||||
it 'updates atom feed link' do
|
||||
visit_issues(milestone_title: '', assignee_username: user.username)
|
||||
visit issues_dashboard_path(milestone_title: '', assignee_username: user.username)
|
||||
click_button 'Actions'
|
||||
|
||||
link = find_link('Subscribe to RSS feed')
|
||||
params = CGI.parse(URI.parse(link[:href]).query)
|
||||
|
|
@ -59,40 +71,47 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
|
|||
end
|
||||
|
||||
context 'filtering by label' do
|
||||
let(:label) { create(:label, project: project) }
|
||||
let!(:label_link) { create(:label_link, label: label, target: issue) }
|
||||
before do
|
||||
visit issues_dashboard_path
|
||||
end
|
||||
|
||||
it 'shows all issues with the selected label' do
|
||||
input_filtered_search("label:=~#{label.title}")
|
||||
select_tokens 'Label', '=', label.title, submit: true
|
||||
|
||||
page.within 'ul.content-list' do
|
||||
expect(page).to have_content issue.title
|
||||
expect(page).not_to have_content issue2.title
|
||||
end
|
||||
expect(page).to have_content issue.title
|
||||
expect(page).not_to have_content issue2.title
|
||||
end
|
||||
|
||||
it 'removes duplicate labels' do
|
||||
select_tokens 'Label', '='
|
||||
send_keys 'bu'
|
||||
|
||||
expect_suggestion('bug')
|
||||
expect_suggestion_count(3) # Expect None, Any, and bug
|
||||
end
|
||||
end
|
||||
|
||||
context 'sorting' do
|
||||
before do
|
||||
visit_issues(assignee_username: user.username)
|
||||
visit issues_dashboard_path(assignee_username: user.username)
|
||||
end
|
||||
|
||||
it 'remembers last sorting value' do
|
||||
pajamas_sort_by(s_('SortOptions|Created date'))
|
||||
visit_issues(assignee_username: user.username)
|
||||
click_button 'Created date'
|
||||
click_button 'Updated date'
|
||||
|
||||
expect(page).to have_button('Created date')
|
||||
visit issues_dashboard_path(assignee_username: user.username)
|
||||
|
||||
expect(page).to have_button('Updated date')
|
||||
end
|
||||
|
||||
it 'keeps sorting issues after visiting Projects Issues page' do
|
||||
pajamas_sort_by(s_('SortOptions|Created date'))
|
||||
click_button 'Created date'
|
||||
click_button 'Due date'
|
||||
|
||||
visit project_issues_path(project)
|
||||
|
||||
expect(page).to have_button('Created date')
|
||||
expect(page).to have_button('Due date')
|
||||
end
|
||||
end
|
||||
|
||||
def visit_issues(...)
|
||||
visit issues_dashboard_path(...)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ require 'spec_helper'
|
|||
RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
|
||||
include FilteredSearchHelpers
|
||||
|
||||
let(:current_user) { create :user }
|
||||
let(:user) { current_user } # Shared examples depend on this being available
|
||||
let!(:public_project) { create(:project, :public) }
|
||||
let(:project) { create(:project) }
|
||||
let(:project_with_issues_disabled) { create(:project, :issues_disabled) }
|
||||
let!(:authored_issue) { create :issue, author: current_user, project: project }
|
||||
let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
|
||||
let!(:assigned_issue) { create :issue, assignees: [current_user], project: project }
|
||||
let!(:other_issue) { create :issue, project: project }
|
||||
let_it_be(:current_user) { create :user }
|
||||
let_it_be(:user) { current_user } # Shared examples depend on this being available
|
||||
let_it_be(:public_project) { create(:project, :public) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:project_with_issues_disabled) { create(:project, :issues_disabled) }
|
||||
let_it_be(:authored_issue) { create :issue, author: current_user, project: project }
|
||||
let_it_be(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project }
|
||||
let_it_be(:assigned_issue) { create :issue, assignees: [current_user], project: project }
|
||||
let_it_be(:other_issue) { create :issue, project: project }
|
||||
|
||||
before do
|
||||
[project, project_with_issues_disabled].each { |project| project.add_maintainer(current_user) }
|
||||
|
|
@ -23,16 +23,16 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
|
|||
|
||||
it_behaves_like 'a "Your work" page with sidebar and breadcrumbs', :issues_dashboard_path, :issues
|
||||
|
||||
describe 'issues' do
|
||||
describe 'issues', :js do
|
||||
it 'shows issues assigned to current user' do
|
||||
expect(page).to have_content(assigned_issue.title)
|
||||
expect(page).not_to have_content(authored_issue.title)
|
||||
expect(page).not_to have_content(other_issue.title)
|
||||
end
|
||||
|
||||
it 'shows issues when current user is author', :js do
|
||||
reset_filters
|
||||
input_filtered_search("author:=#{current_user.to_reference}")
|
||||
it 'shows issues when current user is author' do
|
||||
click_button 'Clear'
|
||||
select_tokens 'Author', '=', current_user.to_reference, submit: true
|
||||
|
||||
expect(page).to have_content(authored_issue.title)
|
||||
expect(page).to have_content(authored_issue_on_public_project.title)
|
||||
|
|
@ -41,12 +41,21 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
|
|||
end
|
||||
|
||||
it 'state filter tabs work' do
|
||||
find('#state-closed').click
|
||||
expect(page).to have_current_path(issues_dashboard_url(assignee_username: current_user.username, state: 'closed'), url: true)
|
||||
click_link 'Closed'
|
||||
|
||||
expect(page).not_to have_content(assigned_issue.title)
|
||||
expect(page).not_to have_content(authored_issue.title)
|
||||
expect(page).not_to have_content(other_issue.title)
|
||||
end
|
||||
|
||||
it_behaves_like "it has an RSS button with current_user's feed token"
|
||||
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
|
||||
describe 'RSS link' do
|
||||
before do
|
||||
click_button 'Actions'
|
||||
end
|
||||
|
||||
it_behaves_like "it has an RSS link with current_user's feed token"
|
||||
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
|
||||
end
|
||||
end
|
||||
|
||||
describe 'new issue dropdown' do
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Dashboard > label filter', :js, feature_category: :team_planning do
|
||||
include FilteredSearchHelpers
|
||||
|
||||
let(:filtered_search) { find('.filtered-search') }
|
||||
let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
|
||||
let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) }
|
||||
let(:label) { create(:label, title: 'bug', color: '#ff0000') }
|
||||
let(:label2) { create(:label, title: 'bug') }
|
||||
|
||||
before do
|
||||
project.labels << label
|
||||
project2.labels << label2
|
||||
|
||||
sign_in(user)
|
||||
visit issues_dashboard_path
|
||||
|
||||
init_label_search
|
||||
end
|
||||
|
||||
context 'duplicate labels' do
|
||||
it 'removes duplicate labels' do
|
||||
filtered_search.send_keys('bu')
|
||||
|
||||
expect(filter_dropdown).to have_selector('.filter-dropdown-item', text: 'bug', count: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -24,6 +24,7 @@ RSpec.describe 'Group labels', feature_category: :team_planning do
|
|||
end
|
||||
|
||||
it 'shows an edit label button', :js do
|
||||
expect(page).to have_selector('.edit')
|
||||
click_button 'Label actions dropdown'
|
||||
expect(page).to have_link('Edit')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue