Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-04-25 12:18:56 +00:00
parent af69e63b66
commit d2d913b606
147 changed files with 2169 additions and 701 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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"},

View File

@ -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)

View File

@ -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"

View File

@ -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,

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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') }}

View File

@ -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"

View File

@ -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 = () => {

View File

@ -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, {

View File

@ -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;

View File

@ -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',
},
);

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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', {

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -115,6 +115,7 @@
}
.label-actions-list {
font-size: 0;
list-style: none;
flex-shrink: 0;
text-align: right;

View File

@ -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; }

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectBadge < Badge
include EachBatch
belongs_to :project
validates :project, presence: true

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Board < ApplicationRecord
include EachBatch
RECENT_BOARDS_SIZE = 4
belongs_to :group

View File

@ -33,6 +33,7 @@ module BulkImports
event :finish do
transition started: :finished
transition finished: :finished
transition failed: :failed
end

View File

@ -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

View File

@ -14,6 +14,7 @@
class ExternalPullRequest < Ci::ApplicationRecord
include Gitlab::Utils::StrongMemoize
include ShaAttribute
include EachBatch
belongs_to :project

View File

@ -9,6 +9,7 @@ class Label < ApplicationRecord
include Sortable
include FromUnion
include Presentable
include EachBatch
cache_markdown_field :description, pipeline: :single_line

View File

@ -9,6 +9,7 @@ class Milestone < ApplicationRecord
include Importable
include IidRoutes
include UpdatedAtFilterable
include EachBatch
prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectCustomAttribute < ApplicationRecord
include EachBatch
belongs_to :project
validates :project, :key, :value, presence: true

View File

@ -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

View File

@ -2,6 +2,7 @@
class ProtectedTag < ApplicationRecord
include ProtectedRef
include EachBatch
validates :name, uniqueness: { scope: :project_id }
validates :project, presence: true

View File

@ -19,6 +19,7 @@ class Snippet < ApplicationRecord
include AfterCommitQueue
extend ::Gitlab::Utils::Override
include CreatedAtFilterable
include EachBatch
MAX_FILE_COUNT = 10

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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 } }

View File

@ -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')}';

View File

@ -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'

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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).
![Merge request with denied licenses](img/denied_licenses_v15_3.png)
The **Policies** tab in the project's license compliance section displays your project's license
policies. Project maintainers can specify policies in this section.
![Edit Policy](img/policies_maintainer_edit_v14_3.png)
![Add Policy](img/policies_maintainer_add_v14_3.png)
Developers of the project can view the policies configured in a project.
![View Policies](img/policies_v13_0.png)
## 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).
![License Check Approver Rule](img/license-check_v13_4.png)
- 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.

View File

@ -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).

View File

@ -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/).

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -7,6 +7,7 @@ FactoryBot.define do
upload { association(:bulk_import_export_upload) }
status { 0 }
batch_number { 1 }
trait :started do
status { 0 }

View File

@ -20,5 +20,9 @@ FactoryBot.define do
trait :failed do
status { -1 }
end
trait :batched do
batched { true }
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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