Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-04 18:20:01 +00:00
parent 73e15fde38
commit 698fe342b9
107 changed files with 1394 additions and 950 deletions

View File

@ -1,11 +1,13 @@
import $ from 'jquery'; import $ from 'jquery';
import { memoize, throttle } from 'lodash'; import { memoize, throttle } from 'lodash';
import createEventHub from '~/helpers/event_hub_factory';
class DirtySubmitForm { class DirtySubmitForm {
constructor(form) { constructor(form) {
this.form = form; this.form = form;
this.dirtyInputs = []; this.dirtyInputs = [];
this.isDisabled = true; this.isDisabled = true;
this.events = createEventHub();
this.init(); this.init();
} }
@ -36,11 +38,21 @@ class DirtySubmitForm {
this.form.addEventListener('submit', (event) => this.formSubmit(event)); this.form.addEventListener('submit', (event) => this.formSubmit(event));
} }
addInputsListener(callback) {
this.events.$on('input', callback);
}
removeInputsListener(callback) {
this.events.$off('input', callback);
}
updateDirtyInput(event) { updateDirtyInput(event) {
const { target } = event; const { target } = event;
if (!target.dataset.isDirtySubmitInput) return; if (!target.dataset.isDirtySubmitInput) return;
this.events.$emit('input', event);
this.updateDirtyInputs(target); this.updateDirtyInputs(target);
this.toggleSubmission(); this.toggleSubmission();
} }

View File

@ -92,6 +92,9 @@ export default {
hasUpstreamPipelines() { hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0); return Boolean(this.pipeline?.upstream?.length > 0);
}, },
isMultiProjectVizAvailable() {
return Boolean(this.pipeline?.user?.namespace?.crossProjectPipelineAvailable);
},
isStageView() { isStageView() {
return this.viewType === STAGE_VIEW; return this.viewType === STAGE_VIEW;
}, },
@ -178,6 +181,7 @@ export default {
<linked-pipelines-column <linked-pipelines-column
v-if="showUpstreamPipelines" v-if="showUpstreamPipelines"
:config-paths="configPaths" :config-paths="configPaths"
:is-multi-project-viz-available="isMultiProjectVizAvailable"
:linked-pipelines="upstreamPipelines" :linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')" :column-title="__('Upstream')"
:show-links="showJobLinks" :show-links="showJobLinks"
@ -226,6 +230,7 @@ export default {
v-if="showDownstreamPipelines" v-if="showDownstreamPipelines"
class="gl-mr-6" class="gl-mr-6"
:config-paths="configPaths" :config-paths="configPaths"
:is-multi-project-viz-available="isMultiProjectVizAvailable"
:linked-pipelines="downstreamPipelines" :linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')" :column-title="__('Downstream')"
:show-links="showJobLinks" :show-links="showJobLinks"

View File

@ -1,12 +1,29 @@
<script> <script>
import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import {
GlBadge,
GlButton,
GlLink,
GlLoadingIcon,
GlPopover,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import TierBadge from '~/vue_shared/components/tier_badge.vue';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue'; import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import { DOWNSTREAM, UPSTREAM } from './constants'; import { DOWNSTREAM, UPSTREAM } from './constants';
export default { export default {
i18n: {
popover: {
title: s__('Pipelines|Multi-project pipeline graphs'),
description: s__(
'Pipelines|Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines. %{linkStart}Learn More%{linkEnd}',
),
},
},
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
@ -16,7 +33,11 @@ export default {
GlButton, GlButton,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlPopover,
GlSprintf,
TierBadge,
}, },
inject: ['multiProjectHelpPath'],
props: { props: {
columnTitle: { columnTitle: {
type: String, type: String,
@ -26,6 +47,10 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isMultiProjectVizAvailable: {
type: Boolean,
required: true,
},
isLoading: { isLoading: {
type: Boolean, type: Boolean,
required: true, required: true,
@ -90,6 +115,9 @@ export default {
pipelineStatus() { pipelineStatus() {
return this.pipeline.status; return this.pipeline.status;
}, },
popoverContainerId() {
return `popoverContainer-${this.pipeline.id}`;
},
projectName() { projectName() {
return this.pipeline.project.name; return this.pipeline.project.name;
}, },
@ -128,16 +156,21 @@ export default {
<template> <template>
<div <div
ref="linkedPipeline"
v-gl-tooltip
class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1" class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
:class="flexDirection" :class="flexDirection"
:title="tooltipText"
data-qa-selector="child_pipeline" data-qa-selector="child_pipeline"
data-testid="linkedPipeline"
@mouseover="onDownstreamHovered" @mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave" @mouseleave="onDownstreamHoverLeave"
> >
<div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass"> <div
v-gl-tooltip
class="gl-w-full gl-bg-white gl-p-3"
:class="cardSpacingClass"
data-testid="linkedPipelineBody"
data-qa-selector="linked_pipeline_body"
:title="tooltipText"
>
<div class="gl-display-flex gl-pr-3"> <div class="gl-display-flex gl-pr-3">
<ci-status <ci-status
v-if="!pipelineIsLoading" v-if="!pipelineIsLoading"
@ -163,17 +196,38 @@ export default {
</gl-badge> </gl-badge>
</div> </div>
</div> </div>
<div class="gl-display-flex"> <div :id="popoverContainerId" class="gl-display-flex">
<gl-button <gl-button
:id="buttonId" :id="buttonId"
class="gl-shadow-none! gl-rounded-0!" class="gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`" :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`"
:icon="expandedIcon" :icon="expandedIcon"
:aria-label="__('Expand pipeline')" :aria-label="__('Expand pipeline')"
:disabled="!isMultiProjectVizAvailable"
data-testid="expand-pipeline-button" data-testid="expand-pipeline-button"
data-qa-selector="expand_pipeline_button" data-qa-selector="expand_pipeline_button"
@click="onClickLinkedPipeline" @click="onClickLinkedPipeline"
/> />
<gl-popover
v-if="!isMultiProjectVizAvailable"
placement="top"
:target="popoverContainerId"
triggers="hover"
>
<template #title>
<b>{{ $options.i18n.popover.title }}</b>
<tier-badge class="gl-mt-3" tier="premium"
/></template>
<p class="gl-my-0">
<gl-sprintf :message="$options.i18n.popover.description">
<template #link="{ content }"
><gl-link :href="multiProjectHelpPath" class="gl-font-sm" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>
</gl-popover>
</div> </div>
</div> </div>
</template> </template>

View File

@ -28,6 +28,10 @@ export default {
required: true, required: true,
validator: validateConfigPaths, validator: validateConfigPaths,
}, },
isMultiProjectVizAvailable: {
type: Boolean,
required: true,
},
linkedPipelines: { linkedPipelines: {
type: Array, type: Array,
required: true, required: true,
@ -208,6 +212,7 @@ export default {
<linked-pipeline <linked-pipeline
class="gl-display-inline-block" class="gl-display-inline-block"
:is-loading="isLoadingPipeline(pipeline.id)" :is-loading="isLoadingPipeline(pipeline.id)"
:is-multi-project-viz-available="isMultiProjectVizAvailable"
:pipeline="pipeline" :pipeline="pipeline"
:column-title="columnTitle" :column-title="columnTitle"
:type="type" :type="type"

View File

@ -60,6 +60,15 @@ export default {
iid: this.pipelineIid, iid: this.pipelineIid,
}; };
}, },
loading() {
return this.$apollo.queries.jobs.loading;
},
showSkeletonLoader() {
return this.firstLoad && this.loading;
},
showLoadingSpinner() {
return !this.firstLoad && this.loading;
},
}, },
mounted() { mounted() {
eventHub.$on('jobActionPerformed', this.handleJobAction); eventHub.$on('jobActionPerformed', this.handleJobAction);
@ -69,7 +78,7 @@ export default {
}, },
methods: { methods: {
handleJobAction() { handleJobAction() {
this.firstLoad = true; this.firstLoad = false;
this.$apollo.queries.jobs.refetch(); this.$apollo.queries.jobs.refetch();
}, },
@ -98,7 +107,7 @@ export default {
<template> <template>
<div> <div>
<div v-if="$apollo.loading && firstLoad" class="gl-mt-5"> <div v-if="showSkeletonLoader" class="gl-mt-5">
<gl-skeleton-loader :width="1248" :height="73"> <gl-skeleton-loader :width="1248" :height="73">
<circle cx="748.031" cy="37.7193" r="15.0307" /> <circle cx="748.031" cy="37.7193" r="15.0307" />
<circle cx="787.241" cy="37.7193" r="15.0307" /> <circle cx="787.241" cy="37.7193" r="15.0307" />
@ -118,7 +127,7 @@ export default {
<jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" /> <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" />
<gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs"> <gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
<gl-loading-icon v-if="$apollo.loading" size="md" /> <gl-loading-icon v-if="showLoadingSpinner" size="md" />
</gl-intersection-observer> </gl-intersection-observer>
</div> </div>
</template> </template>

View File

@ -1,8 +1,7 @@
<script> <script>
import { GlIcon, GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; import { GlIcon, GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { SCHEDULE_ORIGIN, ICONS } from '../../constants'; import { SCHEDULE_ORIGIN, ICONS } from '../../constants';
@ -18,7 +17,6 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagMixin()],
inject: { inject: {
targetProjectFullPath: { targetProjectFullPath: {
default: '', default: '',
@ -139,28 +137,14 @@ export default {
commitTitle() { commitTitle() {
return this.pipeline?.commit?.title; return this.pipeline?.commit?.title;
}, },
hasAuthor() {
return (
this.commitAuthor?.avatar_url && this.commitAuthor?.path && this.commitAuthor?.username
);
},
userImageAltDescription() {
return this.commitAuthor?.username
? sprintf(__("%{username}'s avatar"), { username: this.commitAuthor.username })
: null;
},
rearrangePipelinesTable() {
return this.glFeatures?.rearrangePipelinesTable;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="pipeline-tags" data-testid="pipeline-url-table-cell"> <div class="pipeline-tags" data-testid="pipeline-url-table-cell">
<template v-if="rearrangePipelinesTable">
<div class="commit-title gl-mb-2" data-testid="commit-title-container"> <div class="commit-title gl-mb-2" data-testid="commit-title-container">
<span v-if="commitTitle" class="gl-display-flex"> <span v-if="commitTitle" class="gl-display-flex">
<tooltip-on-truncate :title="commitTitle" class="flex-truncate-child gl-flex-grow-1"> <tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate">
<gl-link <gl-link
:href="commitUrl" :href="commitUrl"
class="commit-row-message gl-text-gray-900" class="commit-row-message gl-text-gray-900"
@ -214,16 +198,6 @@ export default {
}}</gl-link> }}</gl-link>
<!--End of commit row--> <!--End of commit row-->
</div> </div>
</template>
<gl-link
v-if="!rearrangePipelinesTable"
:href="pipeline.path"
class="gl-text-decoration-underline"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
#{{ pipeline[pipelineKey] }}
</gl-link>
<div class="label-container gl-mt-1"> <div class="label-container gl-mt-1">
<gl-badge <gl-badge
v-if="isScheduled" v-if="isScheduled"

View File

@ -1,85 +0,0 @@
<script>
import { CHILD_VIEW } from '~/pipelines/constants';
import CommitComponent from '~/vue_shared/components/commit.vue';
export default {
components: {
CommitComponent,
},
props: {
pipeline: {
type: Object,
required: true,
},
viewType: {
type: String,
required: true,
},
},
computed: {
commitAuthor() {
let commitAuthorInformation;
if (!this.pipeline || !this.pipeline.commit) {
return null;
}
// 1. person who is an author of a commit might be a GitLab user
if (this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user
// they can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) {
commitAuthorInformation = this.pipeline.commit.author;
// 3. If GitLab user does not have avatar, they might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
commitAuthorInformation = {
...this.pipeline.commit.author,
avatar_url: this.pipeline.commit.author_gravatar_url,
};
}
// 4. If committer is not a GitLab User, they can have a Gravatar
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
return commitAuthorInformation;
},
commitTag() {
return this.pipeline?.ref?.tag;
},
commitRef() {
return this.pipeline?.ref;
},
commitUrl() {
return this.pipeline?.commit?.commit_path;
},
commitShortSha() {
return this.pipeline?.commit?.short_id;
},
commitTitle() {
return this.pipeline?.commit?.title;
},
isChildView() {
return this.viewType === CHILD_VIEW;
},
},
};
</script>
<template>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:merge-request-ref="pipeline.merge_request"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
:show-ref-info="!isChildView"
/>
</template>

View File

@ -3,7 +3,6 @@ import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.v
import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants'; import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants';
import { CHILD_VIEW } from '~/pipelines/constants'; import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelinesTimeago from './time_ago.vue'; import PipelinesTimeago from './time_ago.vue';
export default { export default {
@ -12,7 +11,6 @@ export default {
CiBadge, CiBadge,
PipelinesTimeago, PipelinesTimeago,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
@ -44,9 +42,6 @@ export default {
codeQualityBuildPath() { codeQualityBuildPath() {
return this.pipeline?.details?.code_quality_build_path; return this.pipeline?.details?.code_quality_build_path;
}, },
rearrangePipelinesTable() {
return this.glFeatures?.rearrangePipelinesTable;
},
}, },
}; };
</script> </script>
@ -61,7 +56,7 @@ export default {
:icon-classes="'gl-vertical-align-middle!'" :icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status" data-qa-selector="pipeline_commit_status"
/> />
<pipelines-timeago v-if="rearrangePipelinesTable" class="gl-mt-3" :pipeline="pipeline" /> <pipelines-timeago class="gl-mt-3" :pipeline="pipeline" />
<code-quality-walkthrough <code-quality-walkthrough
v-if="shouldRenderCodeQualityWalkthrough" v-if="shouldRenderCodeQualityWalkthrough"
:step="codeQualityStep" :step="codeQualityStep"

View File

@ -1,16 +1,13 @@
<script> <script>
import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import PipelineMiniGraph from './pipeline_mini_graph.vue'; import PipelineMiniGraph from './pipeline_mini_graph.vue';
import PipelineOperations from './pipeline_operations.vue'; import PipelineOperations from './pipeline_operations.vue';
import PipelineStopModal from './pipeline_stop_modal.vue'; import PipelineStopModal from './pipeline_stop_modal.vue';
import PipelineTriggerer from './pipeline_triggerer.vue'; import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelineUrl from './pipeline_url.vue'; import PipelineUrl from './pipeline_url.vue';
import PipelinesCommit from './pipelines_commit.vue';
import PipelinesStatusBadge from './pipelines_status_badge.vue'; import PipelinesStatusBadge from './pipelines_status_badge.vue';
import PipelinesTimeago from './time_ago.vue';
const DEFAULT_TD_CLASS = 'gl-p-5!'; const DEFAULT_TD_CLASS = 'gl-p-5!';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
@ -22,19 +19,16 @@ export default {
GlTableLite, GlTableLite,
LinkedPipelinesMiniList: () => LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
PipelineMiniGraph, PipelineMiniGraph,
PipelineOperations, PipelineOperations,
PipelinesStatusBadge, PipelinesStatusBadge,
PipelineStopModal, PipelineStopModal,
PipelinesTimeago,
PipelineTriggerer, PipelineTriggerer,
PipelineUrl, PipelineUrl,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
pipelines: { pipelines: {
type: Array, type: Array,
@ -74,18 +68,16 @@ export default {
key: 'status', key: 'status',
label: s__('Pipeline|Status'), label: s__('Pipeline|Status'),
thClass: DEFAULT_TH_CLASSES, thClass: DEFAULT_TH_CLASSES,
columnClass: this.rearrangePipelinesTable ? 'gl-w-15p' : 'gl-w-10p', columnClass: 'gl-w-15p',
tdClass: DEFAULT_TD_CLASS, tdClass: DEFAULT_TD_CLASS,
thAttr: { 'data-testid': 'status-th' }, thAttr: { 'data-testid': 'status-th' },
}, },
{ {
key: 'pipeline', key: 'pipeline',
label: this.rearrangePipelinesTable ? __('Pipeline') : this.pipelineKeyOption.label, label: __('Pipeline'),
thClass: DEFAULT_TH_CLASSES, thClass: DEFAULT_TH_CLASSES,
tdClass: this.rearrangePipelinesTable tdClass: `${DEFAULT_TD_CLASS}`,
? `${DEFAULT_TD_CLASS}` columnClass: 'gl-w-30p',
: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: this.rearrangePipelinesTable ? 'gl-w-30p' : 'gl-w-10p',
thAttr: { 'data-testid': 'pipeline-th' }, thAttr: { 'data-testid': 'pipeline-th' },
}, },
{ {
@ -96,14 +88,6 @@ export default {
columnClass: 'gl-w-10p', columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'triggerer-th' }, thAttr: { 'data-testid': 'triggerer-th' },
}, },
{
key: 'commit',
label: s__('Pipeline|Commit'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'commit-th' },
},
{ {
key: 'stages', key: 'stages',
label: s__('Pipeline|Stages'), label: s__('Pipeline|Stages'),
@ -112,14 +96,6 @@ export default {
columnClass: 'gl-w-quarter', columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' }, thAttr: { 'data-testid': 'stages-th' },
}, },
{
key: 'timeago',
label: s__('Pipeline|Duration'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: this.rearrangePipelinesTable ? 'gl-w-5p' : 'gl-w-15p',
thAttr: { 'data-testid': 'timeago-th' },
},
{ {
key: 'actions', key: 'actions',
thClass: DEFAULT_TH_CLASSES, thClass: DEFAULT_TH_CLASSES,
@ -129,12 +105,7 @@ export default {
}, },
]; ];
return !this.rearrangePipelinesTable return fields;
? fields
: fields.filter((field) => !['commit', 'timeago'].includes(field.key));
},
rearrangePipelinesTable() {
return this.glFeatures?.rearrangePipelinesTable;
}, },
}, },
watch: { watch: {
@ -200,10 +171,6 @@ export default {
<pipeline-triggerer :pipeline="item" /> <pipeline-triggerer :pipeline="item" />
</template> </template>
<template #cell(commit)="{ item }">
<pipelines-commit :pipeline="item" :view-type="viewType" />
</template>
<template #cell(stages)="{ item }"> <template #cell(stages)="{ item }">
<div class="stage-cell"> <div class="stage-cell">
<!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 -->
@ -229,10 +196,6 @@ export default {
</div> </div>
</template> </template>
<template #cell(timeago)="{ item }">
<pipelines-timeago :pipeline="item" />
</template>
<template #cell(actions)="{ item }"> <template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
</template> </template>

View File

@ -1,6 +1,5 @@
<script> <script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
@ -8,7 +7,7 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
components: { GlIcon }, components: { GlIcon },
mixins: [timeagoMixin, glFeatureFlagMixin()], mixins: [timeagoMixin],
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
@ -54,14 +53,11 @@ export default {
showSkipped() { showSkipped() {
return !this.duration && !this.finishedTime && this.skipped; return !this.duration && !this.finishedTime && this.skipped;
}, },
shouldDisplayAsBlock() {
return this.glFeatures?.rearrangePipelinesTable;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="{ 'gl-display-block': shouldDisplayAsBlock }"> <div class="gl-display-block">
<span v-if="showInProgress" data-testid="pipeline-in-progress"> <span v-if="showInProgress" data-testid="pipeline-in-progress">
<gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" /> <gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" />
<gl-icon <gl-icon

View File

@ -8,7 +8,7 @@ Vue.use(VueApollo);
const createPipelinesDetailApp = ( const createPipelinesDetailApp = (
selector, selector,
apolloProvider, apolloProvider,
{ pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {}, { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag, multiProjectHelpPath } = {},
) => { ) => {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
@ -22,6 +22,7 @@ const createPipelinesDetailApp = (
pipelineProjectPath, pipelineProjectPath,
pipelineIid, pipelineIid,
graphqlResourceEtag, graphqlResourceEtag,
multiProjectHelpPath,
}, },
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`); reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);

View File

@ -1,12 +1,7 @@
<script> <script>
import { GlLink, GlIcon } from '@gitlab/ui'; import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
function buildDocsLinkStart(path) {
return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`;
}
const NoteableTypeText = { const NoteableTypeText = {
Issue: __('issue'), Issue: __('issue'),
Epic: __('epic'), Epic: __('epic'),
@ -17,6 +12,7 @@ export default {
components: { components: {
GlIcon, GlIcon,
GlLink, GlLink,
GlSprintf,
}, },
props: { props: {
isLocked: { isLocked: {
@ -59,20 +55,6 @@ export default {
noteableTypeText() { noteableTypeText() {
return NoteableTypeText[this.noteableType]; return NoteableTypeText[this.noteableType];
}, },
confidentialAndLockedDiscussionText() {
return sprintf(
__(
'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.',
),
{
noteableTypeText: this.noteableTypeText,
confidentialLinkStart: buildDocsLinkStart(this.confidentialNoteableDocsPath),
lockedLinkStart: buildDocsLinkStart(this.lockedNoteableDocsPath),
linkEnd: '</a>',
},
false,
);
},
confidentialContextText() { confidentialContextText() {
return sprintf(__('This is a confidential %{noteableTypeText}.'), { return sprintf(__('This is a confidential %{noteableTypeText}.'), {
noteableTypeText: this.noteableTypeText, noteableTypeText: this.noteableTypeText,
@ -91,9 +73,23 @@ export default {
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> <span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
<span <span>
v-html="confidentialAndLockedDiscussionText /* eslint-disable-line vue/no-v-html */" <gl-sprintf
></span> :message="
__(
'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}.',
)
"
>
<template #noteableTypeText>{{ noteableTypeText }}</template>
<template #confidentialLink="{ content }">
<gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ content }}</gl-link>
</template>
<template #lockedLink="{ content }">
<gl-link :href="lockedNoteableDocsPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
{{ {{
__("People without permission will never get a notification and won't be able to comment.") __("People without permission will never get a notification and won't be able to comment.")
}} }}

View File

@ -0,0 +1,40 @@
<script>
import { GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
const gitlabTiers = {
free: s__('GitlabTiers|Free'),
premium: s__('GitlabTiers|Premium'),
ultimate: s__('GitlabTiers|Ultimate'),
};
export default {
components: {
GlBadge,
GlIcon,
},
props: {
tier: {
type: String,
required: true,
validator: (value) => Object.keys(gitlabTiers).includes(value),
},
size: {
type: String,
required: false,
default: 'md',
},
},
computed: {
tierName() {
return gitlabTiers[this.tier];
},
},
};
</script>
<template>
<gl-badge :size="size" class="gl-text-purple-600! gl-font-weight-bold gl-bg-purple-50!">
<gl-icon name="license" /> {{ tierName }}
</gl-badge>
</template>

View File

@ -5,6 +5,8 @@ class Admin::CohortsController < Admin::ApplicationController
feature_category :devops_reports feature_category :devops_reports
urgency :low
def index def index
@cohorts = load_cohorts @cohorts = load_cohorts
track_cohorts_visit track_cohorts_visit

View File

@ -9,6 +9,8 @@ class Admin::DevOpsReportController < Admin::ApplicationController
feature_category :devops_reports feature_category :devops_reports
urgency :low
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def show def show
@metric = DevOpsReport::Metric.order(:created_at).last&.present @metric = DevOpsReport::Metric.order(:created_at).last&.present

View File

@ -2,6 +2,8 @@
class Admin::InstanceReviewController < Admin::ApplicationController class Admin::InstanceReviewController < Admin::ApplicationController
feature_category :devops_reports feature_category :devops_reports
urgency :low
def index def index
redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}") redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}")
end end

View File

@ -7,6 +7,8 @@ class Admin::UsageTrendsController < Admin::ApplicationController
feature_category :devops_reports feature_category :devops_reports
urgency :low
def index def index
end end
end end

View File

@ -45,7 +45,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml) push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml)
push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml) push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)
push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml)
push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml) push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml)
# Usage data feature flags # Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml) push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml)

View File

@ -16,9 +16,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts] before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action do
push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml)
end
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? } before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }

View File

@ -47,6 +47,15 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
id id
iid iid
complete complete
user {
__typename
id
namespace {
__typename
id
crossProjectPipelineAvailable
}
}
usesNeeds usesNeeds
userPermissions { userPermissions {
updatePipeline updatePipeline

View File

@ -38,7 +38,7 @@ module Types
description(enum_mod.description) if use_description description(enum_mod.description) if use_description
enum_mod.definition.each do |key, content| enum_mod.definition.each do |key, content|
value(key.to_s.upcase, **content) value(key.to_s.upcase, value: key.to_s, description: content[:description])
end end
end end
# rubocop: enable Graphql/Descriptions # rubocop: enable Graphql/Descriptions

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Projects
module PipelineHelper
def js_pipeline_details_data(project, pipeline)
{
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
multi_project_help_path: help_page_path('ci/pipelines/multi_project_pipelines.md', anchor: 'multi-project-pipeline-visualization'),
pipeline_iid: pipeline.iid,
pipeline_project_path: project.full_path
}
end
end
end

View File

@ -12,21 +12,32 @@ module Boards
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def metadata def metadata(required_fields = [:issue_count, :total_issue_weight])
issuables = item_model.arel_table fields = metadata_fields(required_fields)
keys = metadata_fields.keys keys = fields.keys
# TODO: eliminate need for SQL literal fragment # TODO: eliminate need for SQL literal fragment
columns = Arel.sql(metadata_fields.values_at(*keys).join(', ')) columns = Arel.sql(fields.values_at(*keys).join(', '))
results = item_model.where(id: init_collection.select(issuables[:id])).pluck(columns) results = item_model.where(id: collection_ids)
results = query_additions(results, required_fields)
results = results.select(columns)
Hash[keys.zip(results.flatten)] Hash[keys.zip(results.pluck(columns).flatten)]
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private private
def metadata_fields # override if needed
{ size: 'COUNT(*)' } def query_additions(items, required_fields)
items
end
def collection_ids
@collection_ids ||= init_collection.select(item_model.arel_table[:id])
end
def metadata_fields(required_fields)
required_fields&.include?(:issue_count) ? { size: 'COUNT(*)' } : {}
end end
def order(items) def order(items)

View File

@ -4,11 +4,13 @@ module IssuableLinks
class DestroyService < BaseService class DestroyService < BaseService
include IncidentManagement::UsageData include IncidentManagement::UsageData
attr_reader :link, :current_user attr_reader :link, :current_user, :source, :target
def initialize(link, user) def initialize(link, user)
@link = link @link = link
@current_user = user @current_user = user
@source = link.source
@target = link.target
end end
def execute def execute
@ -22,6 +24,11 @@ module IssuableLinks
private private
def create_notes
SystemNoteService.unrelate_issuable(source, target, current_user)
SystemNoteService.unrelate_issuable(target, source, current_user)
end
def after_destroy def after_destroy
create_notes create_notes
track_event track_event

View File

@ -4,23 +4,10 @@ module IssueLinks
class DestroyService < IssuableLinks::DestroyService class DestroyService < IssuableLinks::DestroyService
private private
def source
@source ||= link.source
end
def target
@target ||= link.target
end
def permission_to_remove_relation? def permission_to_remove_relation?
can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target) can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target)
end end
def create_notes
SystemNoteService.unrelate_issue(source, target, current_user)
SystemNoteService.unrelate_issue(target, source, current_user)
end
def track_event def track_event
track_incident_action(current_user, target, :incident_unrelate) track_incident_action(current_user, target, :incident_unrelate)
end end

View File

@ -53,8 +53,8 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref) ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
end end
def unrelate_issue(noteable, noteable_ref, user) def unrelate_issuable(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref) ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issuable(noteable_ref)
end end
# Called when the due_date of a Noteable is changed # Called when the due_date of a Noteable is changed

View File

@ -26,8 +26,8 @@ module SystemNotes
# "removed the relation with gitlab-foss#9001" # "removed the relation with gitlab-foss#9001"
# #
# Returns the created Note object # Returns the created Note object
def unrelate_issue(noteable_ref) def unrelate_issuable(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}" body = "removed the relation with #{noteable_ref.to_reference(noteable.resource_parent)}"
issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue) issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue)

View File

@ -42,10 +42,20 @@ module WebHooks
hook.failed! hook.failed!
end end
end end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
raise if raise_lock_error?
end end
def lock_name def lock_name
"web_hooks:update_hook_failure_state:#{hook.id}" "web_hooks:update_hook_failure_state:#{hook.id}"
end end
# Allow an error to be raised after failing to obtain a lease only if the hook
# is not already in the correct failure state.
def raise_lock_error?
hook.reset # Reload so properties are guaranteed to be current.
hook.executable? != (response_category == :ok)
end
end end
end end

View File

@ -29,4 +29,4 @@
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } } #js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors = render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } } .js-pipeline-details-vue{ data: js_pipeline_details_data(@project, @pipeline) }

View File

@ -31,6 +31,7 @@
- continuous_delivery - continuous_delivery
- continuous_integration - continuous_integration
- continuous_integration_scaling - continuous_integration_scaling
- continuous_verification
- database - database
- dataops - dataops
- delivery - delivery
@ -74,7 +75,6 @@
- kubernetes_management - kubernetes_management
- license - license
- license_compliance - license_compliance
- live_preview
- logging - logging
- memory - memory
- merge_trains - merge_trains
@ -110,7 +110,6 @@
- secrets_management - secrets_management
- security_benchmarking - security_benchmarking
- security_orchestration - security_orchestration
- self_monitoring
- service_desk - service_desk
- service_ping - service_ping
- sharding - sharding
@ -119,7 +118,6 @@
- static_application_security_testing - static_application_security_testing
- static_site_editor - static_site_editor
- subgroups - subgroups
- synthetic_monitoring
- team_planning - team_planning
- tracing - tracing
- usage_ping - usage_ping

View File

@ -1,7 +1,7 @@
--- ---
name: ci_pending_builds_maintain_denormalized_data name: ci_pending_builds_maintain_denormalized_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75425 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75425
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496
milestone: '14.6' milestone: '14.6'
type: development type: development
group: group::pipeline execution group: group::pipeline execution

View File

@ -1,7 +1,7 @@
--- ---
name: ci_pending_builds_queue_source name: ci_pending_builds_queue_source
introduced_by_url: introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350884 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496
milestone: '14.0' milestone: '14.0'
type: development type: development
group: group::pipeline execution group: group::pipeline execution

View File

@ -1,7 +1,7 @@
--- ---
name: ci_queuing_use_denormalized_data_strategy name: ci_queuing_use_denormalized_data_strategy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76543 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76543
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496
milestone: '14.6' milestone: '14.6'
type: development type: development
group: group::pipeline execution group: group::pipeline execution

View File

@ -1,8 +1,8 @@
--- ---
name: rearrange_pipelines_table name: container_registry_follow_redirects_middleware
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72545 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81056
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343286 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353291
milestone: '14.8' milestone: '14.9'
type: development type: development
group: group::pipeline execution group: group::package
default_enabled: true default_enabled: false

View File

@ -62,3 +62,27 @@
- 'i_testing_group_code_coverage_visit_total' - 'i_testing_group_code_coverage_visit_total'
- 'i_testing_load_performance_widget_total' - 'i_testing_load_performance_widget_total'
- 'i_testing_metrics_report_widget_total' - 'i_testing_metrics_report_widget_total'
- name: xmau_plan
operator: OR
source: redis
time_frame: [7d, 28d]
events:
- users_creating_work_items
- users_updating_work_item_title
feature_flag: track_work_items_activity
- name: xmau_project_management
operator: OR
source: redis
time_frame: [7d, 28d]
events:
- users_creating_work_items
- users_updating_work_item_title
feature_flag: track_work_items_activity
- name: users_work_items
operator: OR
source: redis
time_frame: [7d, 28d]
events:
- users_creating_work_items
- users_updating_work_item_title
feature_flag: track_work_items_activity

View File

@ -0,0 +1,21 @@
---
key_path: counts_monthly.aggregated_metrics.xmau_plan
description: Unique users interacting with Plan features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 28d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_monthly.aggregated_metrics.xmau_project_management
description: Unique users interacting with Project Management features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 28d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_monthly.aggregated_metrics.users_work_items
description: Unique users interacting with work items
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::product planning
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 28d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_weekly.aggregated_metrics.xmau_plan
description: Unique users interacting with Plan features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 7d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_weekly.aggregated_metrics.xmau_project_management
description: Unique users interacting with Project Management features
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::project management
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 7d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts_weekly.aggregated_metrics.users_work_items
description: Unique users interacting with work items
product_category: team planning
product_section: dev
product_stage: plan
product_group: group::product planning
value_type: number
status: active
milestone: '14.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81336
time_frame: 7d
data_source: redis_hll
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated. announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2022-05-22 # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. removal_date: "2022-05-22" # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true breaking_change: true # If this deprecation is a breaking change, set this value to true
reporter: jacobvosmaer-gitlab # GitLab username of the person reporting the deprecation reporter: jacobvosmaer-gitlab # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below. body: | # Do not modify this line, instead modify the lines below.

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated. announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. announcement_date: "2021-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2021-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. removal_date: "2021-05-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true breaking_change: true # If this deprecation is a breaking change, set this value to true
body: | # Do not modify this line, instead modify the lines below. body: | # Do not modify this line, instead modify the lines below.
For those using Dependency Scanning for Python projects, we are deprecating the default `gemnasium-python:2` image which uses Python 3.6 as well as the custom `gemnasium-python:2-python-3.9` image which uses Python 3.9. The new default image as of GitLab 15.0 will be for Python 3.9 as it is a [supported version](https://endoflife.date/python) and 3.6 [is no longer supported](https://endoflife.date/python). For those using Dependency Scanning for Python projects, we are deprecating the default `gemnasium-python:2` image which uses Python 3.6 as well as the custom `gemnasium-python:2-python-3.9` image which uses Python 3.9. The new default image as of GitLab 15.0 will be for Python 3.9 as it is a [supported version](https://endoflife.date/python) and 3.6 [is no longer supported](https://endoflife.date/python).

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated. announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2022-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. removal_date: "2022-05-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # If this deprecation is a breaking change, set this value to true breaking_change: true # If this deprecation is a breaking change, set this value to true
body: | # Do not modify this line, instead modify the lines below. body: | # Do not modify this line, instead modify the lines below.
By default, all new applications expire access tokens after 2 hours. In GitLab 14.2 and earlier, OAuth access tokens By default, all new applications expire access tokens after 2 hours. In GitLab 14.2 and earlier, OAuth access tokens

View File

@ -2,7 +2,7 @@
announcement_milestone: "14.0" # The milestone when this feature was first announced as deprecated. announcement_milestone: "14.0" # The milestone when this feature was first announced as deprecated.
announcement_date: "2021-06-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. announcement_date: "2021-06-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is planned to be removed removal_milestone: "15.0" # The milestone when this feature is planned to be removed
removal_date: 2022-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. removal_date: "2022-05-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: yes # If this deprecation is a breaking change, set this value to true breaking_change: yes # If this deprecation is a breaking change, set this value to true
body: | # Do not modify this line, instead modify the lines below. body: | # Do not modify this line, instead modify the lines below.
The OAuth implicit grant authorization flow will be removed in our next major release, GitLab 15.0. Any applications that use OAuth implicit grant should switch to alternative [supported OAuth flows](https://docs.gitlab.com/ee/api/oauth2.html). The OAuth implicit grant authorization flow will be removed in our next major release, GitLab 15.0. Any applications that use OAuth implicit grant should switch to alternative [supported OAuth flows](https://docs.gitlab.com/ee/api/oauth2.html).

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexToIssues < Gitlab::Database::Migration[1.0]
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_issues_on_id_and_weight'
def up
add_concurrent_index :issues, [:id, :weight], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :issues, INDEX_NAME
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class ScheduleMigratePersonalNamespaceProjectMaintainerToOwner < Gitlab::Database::Migration[1.0]
MIGRATION = 'MigratePersonalNamespaceProjectMaintainerToOwner'
INTERVAL = 2.minutes
BATCH_SIZE = 1_000
SUB_BATCH_SIZE = 200
disable_ddl_transaction!
def up
queue_batched_background_migration(
MIGRATION,
:members,
:id,
job_interval: INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
688232dde01ea4e8574dca73459094264bde405d799ecaf1a5867adb72576b98

View File

@ -0,0 +1 @@
84346c2f608792f259ab91dbc2c8aac8397a2997f890f8e077aad809276bb7cd

View File

@ -27884,6 +27884,8 @@ CREATE INDEX index_issues_on_description_trigram ON issues USING gin (descriptio
CREATE INDEX index_issues_on_duplicated_to_id ON issues USING btree (duplicated_to_id) WHERE (duplicated_to_id IS NOT NULL); CREATE INDEX index_issues_on_duplicated_to_id ON issues USING btree (duplicated_to_id) WHERE (duplicated_to_id IS NOT NULL);
CREATE INDEX index_issues_on_id_and_weight ON issues USING btree (id, weight);
CREATE INDEX index_issues_on_incident_issue_type ON issues USING btree (issue_type) WHERE (issue_type = 1); CREATE INDEX index_issues_on_incident_issue_type ON issues USING btree (issue_type) WHERE (issue_type = 1);
CREATE INDEX index_issues_on_last_edited_by_id ON issues USING btree (last_edited_by_id); CREATE INDEX index_issues_on_last_edited_by_id ON issues USING btree (last_edited_by_id);

View File

@ -208,13 +208,18 @@ When the number exceeds the limit the page displays an alert and links to a pagi
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51401) in GitLab 11.10. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51401) in GitLab 11.10.
The number of pipelines that can be created in a single push is 4. When pushing multiple changes with a single Git push, like multiple tags or branches,
This limit prevents the accidental creation of pipelines when `git push --all` only four tag or branch pipelines can be triggered. This limit prevents the accidental
or `git push --mirror` is used. creation of a large number of pipelines when using `git push --all` or `git push --mirror`.
This limit does not affect any of the updated merge request pipelines. [Merge request pipelines](../ci/pipelines/merge_request_pipelines.md) are not limited.
All updated merge requests have a pipeline created when using If the Git push updates multiple merge requests at the same time, a merge request pipeline
[merge request pipelines](../ci/pipelines/merge_request_pipelines.md). can trigger for every updated merge request.
To remove the limit so that any number of pipelines can trigger for a single Git push event,
administrators can enable the `git_push_create_all_pipelines` [feature flag](feature_flags.md).
Enabling this feature flag is not recommended, as it can cause excessive load on the GitLab
instance if too many changes are pushed at once and a flood of pipelines are created accidentally.
## Retention of activity history ## Retention of activity history

View File

@ -10759,10 +10759,11 @@ Represents an epic board list.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="epiclistcollapsed"></a>`collapsed` | [`Boolean`](#boolean) | Indicates if this list is collapsed for this user. | | <a id="epiclistcollapsed"></a>`collapsed` | [`Boolean`](#boolean) | Indicates if this list is collapsed for this user. |
| <a id="epiclistepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. | | <a id="epiclistepicscount"></a>`epicsCount` **{warning-solid}** | [`Int`](#int) | **Deprecated** in 14.9. This was renamed. Use: `metadata`. |
| <a id="epiclistid"></a>`id` | [`BoardsEpicListID!`](#boardsepiclistid) | Global ID of the board list. | | <a id="epiclistid"></a>`id` | [`BoardsEpicListID!`](#boardsepiclistid) | Global ID of the board list. |
| <a id="epiclistlabel"></a>`label` | [`Label`](#label) | Label of the list. | | <a id="epiclistlabel"></a>`label` | [`Label`](#label) | Label of the list. |
| <a id="epiclistlisttype"></a>`listType` | [`String!`](#string) | Type of the list. | | <a id="epiclistlisttype"></a>`listType` | [`String!`](#string) | Type of the list. |
| <a id="epiclistmetadata"></a>`metadata` | [`EpicListMetadata`](#epiclistmetadata) | Epic list metatada. |
| <a id="epiclistposition"></a>`position` | [`Int`](#int) | Position of the list within the board. | | <a id="epiclistposition"></a>`position` | [`Int`](#int) | Position of the list within the board. |
| <a id="epiclisttitle"></a>`title` | [`String!`](#string) | Title of the list. | | <a id="epiclisttitle"></a>`title` | [`String!`](#string) | Title of the list. |
@ -10784,6 +10785,17 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="epiclistepicsfilters"></a>`filters` | [`EpicFilters`](#epicfilters) | Filters applied when selecting epics in the board list. | | <a id="epiclistepicsfilters"></a>`filters` | [`EpicFilters`](#epicfilters) | Filters applied when selecting epics in the board list. |
### `EpicListMetadata`
Represents epic board list metadata.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="epiclistmetadataepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. |
| <a id="epiclistmetadatatotalweight"></a>`totalWeight` | [`Int`](#int) | Total weight of all issues in the list. Available only when feature flag `epic_board_total_weight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
### `EpicPermissions` ### `EpicPermissions`
Check permissions for the current user on an epic. Check permissions for the current user on an epic.
@ -15271,6 +15283,7 @@ Represents the security scan information.
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. | | <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. |
| <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. | | <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. |
| <a id="scanstatus"></a>`status` | [`ScanStatus!`](#scanstatus) | Indicates the status of the scan. |
| <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. | | <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. |
### `ScanExecutionPolicy` ### `ScanExecutionPolicy`
@ -18142,6 +18155,20 @@ Size of UI component in SAST configuration page.
| <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. | | <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. |
| <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. | | <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. |
### `ScanStatus`
The status of the security scan.
| Value | Description |
| ----- | ----------- |
| <a id="scanstatuscreated"></a>`CREATED` | The scan has been created. |
| <a id="scanstatusjob_failed"></a>`JOB_FAILED` | The related CI build failed. |
| <a id="scanstatuspreparation_failed"></a>`PREPARATION_FAILED` | Report couldn't be prepared. |
| <a id="scanstatuspreparing"></a>`PREPARING` | Preparing the report for the scan. |
| <a id="scanstatuspurged"></a>`PURGED` | Report for the scan has been removed from the database. |
| <a id="scanstatusreport_error"></a>`REPORT_ERROR` | The report artifact provided by the CI build couldn't be parsed. |
| <a id="scanstatussucceeded"></a>`SUCCEEDED` | The report has been successfully prepared. |
### `SecurityReportTypeEnum` ### `SecurityReportTypeEnum`
| Value | Description | | Value | Description |

View File

@ -12,9 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - It's enabled on GitLab.com. > - It's enabled on GitLab.com.
> - It's recommended for production use. > - It's recommended for production use.
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
Usage Trends gives you an overview of how much data your instance contains, and how quickly this volume is changing over time. Usage Trends gives you an overview of how much data your instance contains, and how quickly this volume is changing over time.
Usage Trends data refreshes daily. Usage Trends data refreshes daily.

View File

@ -219,7 +219,7 @@ security issues:
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](../../update/deprecations.md#vulnerability-check) This feature is in its end-of-life process. It is [deprecated](../../update/deprecations.md#vulnerability-check)
for use in GitLab 14.8, and is planned for removal in GitLab 15.0. Users should migrate to the new for use in GitLab 14.8, and is planned for removal in GitLab 15.0. Users should migrate to the new
[Security Approval Policies](policies/#scan-result-policy-editor). [Security Approval Policies](policies/scan-result-policies.md).
To prevent a merge request introducing a security vulnerability in a project, enable the To prevent a merge request introducing a security vulnerability in a project, enable the
Vulnerability-Check rule. While this rule is enabled, additional merge request approval by Vulnerability-Check rule. While this rule is enabled, additional merge request approval by

View File

@ -12,16 +12,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) to GitLab Free in 14.5. > - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) to GitLab Free in 14.5.
> - Support for Omnibus installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5. > - Support for Omnibus installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5.
You can use GitLab CI/CD to safely deploy to and update your Kubernetes clusters. You can use a GitLab CI/CD workflow to safely deploy to and update your Kubernetes clusters.
To do so, you install a GitLab agent in your cluster. Then in your GitLab CI/CD pipelines, To do so, you must first [install an agent in your cluster](install/index.md). When done, you have a Kubernetes context and can
you can refer to the cluster connection as a Kubernetes context. run Kubernetes API commands in your GitLab CI/CD pipeline.
Then you can run Kubernetes API commands as part of your GitLab CI/CD pipeline.
To ensure access to your cluster is safe: To ensure access to your cluster is safe:
- Each agent has a separate context (`kubecontext`). - Each agent has a separate context (`kubecontext`).
- Only the project where the agent is, and any additional projects you authorize can access the agent in your cluster. - Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster.
You do not need to have a runner in the cluster with the agent. You do not need to have a runner in the cluster with the agent.

View File

@ -14,7 +14,22 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Renamed](https://gitlab.com/groups/gitlab-org/-/epics/7167) from "GitLab Kubernetes Agent" to "GitLab agent for Kubernetes" in GitLab 14.6. > - [Renamed](https://gitlab.com/groups/gitlab-org/-/epics/7167) from "GitLab Kubernetes Agent" to "GitLab agent for Kubernetes" in GitLab 14.6.
You can connect your Kubernetes cluster with GitLab to deploy, manage, You can connect your Kubernetes cluster with GitLab to deploy, manage,
and monitor your cloud-native solutions. You can choose from two primary workflows. and monitor your cloud-native solutions.
To connect a Kubernetes cluster to GitLab, you must first [install an agent in your cluster](install/index.md).
The agent runs in the cluster, and you can use it to:
- Communicate with a cluster, which is behind a firewall or NAT.
- Access API endpoints in a cluster in real time.
- Push information about events happening in the cluster.
- Enable a cache of Kubernetes objects, which are kept up-to-date with very low latency.
For more details about the agent's purpose and architecture, see the [architecture documentation](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/architecture.md).
## Workflows
You can choose from two primary workflows.
In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and
any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based, any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based,
@ -23,8 +38,6 @@ because the cluster is pulling updates from your GitLab repository.
In a [**CI/CD** workflow](ci_cd_tunnel.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API. In a [**CI/CD** workflow](ci_cd_tunnel.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API.
This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster. This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster.
Both of these workflows require you to [install an agent in your cluster](install/index.md).
## Supported cluster versions ## Supported cluster versions
GitLab supports the following Kubernetes versions. You can upgrade your GitLab supports the following Kubernetes versions. You can upgrade your

View File

@ -12,7 +12,7 @@ module API
desc 'Get the current application statistics' do desc 'Get the current application statistics' do
success Entities::ApplicationStatistics success Entities::ApplicationStatistics
end end
get "application/statistics" do get "application/statistics", urgency: :low do
counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS) counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
present counts, with: Entities::ApplicationStatistics present counts, with: Entities::ApplicationStatistics
end end

View File

@ -10,6 +10,21 @@ module ContainerRegistry
REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features' REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
REGISTRY_TAG_DELETE_FEATURE = 'tag_delete' REGISTRY_TAG_DELETE_FEATURE = 'tag_delete'
ALLOWED_REDIRECT_SCHEMES = %w[http https].freeze
REDIRECT_OPTIONS = {
clear_authorization_header: true,
limit: 3,
cookies: [],
callback: -> (response_env, request_env) do
request_env.request_headers.delete(::FaradayMiddleware::FollowRedirects::AUTH_HEADER)
redirect_to = request_env.url
unless redirect_to.scheme.in?(ALLOWED_REDIRECT_SCHEMES)
raise ArgumentError, "Invalid scheme for #{redirect_to}"
end
end
}.freeze
def self.supports_tag_delete? def self.supports_tag_delete?
with_dummy_client(return_value_if_disabled: false) do |client| with_dummy_client(return_value_if_disabled: false) do |client|
client.supports_tag_delete? client.supports_tag_delete?
@ -136,6 +151,10 @@ module ContainerRegistry
def faraday_blob def faraday_blob
@faraday_blob ||= faraday_base do |conn| @faraday_blob ||= faraday_base do |conn|
initialize_connection(conn, @options) initialize_connection(conn, @options)
if Feature.enabled?(:container_registry_follow_redirects_middleware)
conn.use ::FaradayMiddleware::FollowRedirects, REDIRECT_OPTIONS
end
end end
end end
end end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Migrates personal namespace project `maintainer` memberships (for the associated user only) to OWNER
# Does not create any missing records, simply migrates existing ones
class MigratePersonalNamespaceProjectMaintainerToOwner
include Gitlab::Database::DynamicModelHelpers
def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms)
parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id)
parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
batch_metrics.time_operation(:update_all) do
sub_batch.update_all('access_level = 50')
end
pause_ms = 0 if pause_ms < 0
sleep(pause_ms * 0.001)
end
end
def batch_metrics
@batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
end
private
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
# members of projects within their own personal namespace
# rubocop: disable CodeReuse/ActiveRecord
define_batchable_model(:members, connection: ApplicationRecord.connection)
.where(source_key_column => start_id..stop_id)
.joins("INNER JOIN projects ON members.source_id = projects.id")
.joins("INNER JOIN namespaces ON projects.namespace_id = namespaces.id")
.where(type: 'ProjectMember')
.where("namespaces.type = 'User'")
.where('members.access_level < 50')
.where('namespaces.owner_id = members.user_id')
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end

View File

@ -16777,6 +16777,15 @@ msgstr ""
msgid "GithubIntegration|This requires mirroring your GitHub repository to this project. %{docs_link}" msgid "GithubIntegration|This requires mirroring your GitHub repository to this project. %{docs_link}"
msgstr "" msgstr ""
msgid "GitlabTiers|Free"
msgstr ""
msgid "GitlabTiers|Premium"
msgstr ""
msgid "GitlabTiers|Ultimate"
msgstr ""
msgid "Gitpod" msgid "Gitpod"
msgstr "" msgstr ""
@ -26990,6 +26999,9 @@ msgstr ""
msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners." msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners."
msgstr "" msgstr ""
msgid "Pipelines|Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines. %{linkStart}Learn More%{linkEnd}"
msgstr ""
msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you." msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you."
msgstr "" msgstr ""
@ -27029,6 +27041,9 @@ msgstr ""
msgid "Pipelines|More Information" msgid "Pipelines|More Information"
msgstr "" msgstr ""
msgid "Pipelines|Multi-project pipeline graphs"
msgstr ""
msgid "Pipelines|No runners detected" msgid "Pipelines|No runners detected"
msgstr "" msgstr ""
@ -27179,9 +27194,6 @@ msgstr ""
msgid "Pipeline|Checking pipeline status." msgid "Pipeline|Checking pipeline status."
msgstr "" msgstr ""
msgid "Pipeline|Commit"
msgstr ""
msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}." msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}."
msgstr "" msgstr ""
@ -27197,9 +27209,6 @@ msgstr ""
msgid "Pipeline|Detached merge request pipeline" msgid "Pipeline|Detached merge request pipeline"
msgstr "" msgstr ""
msgid "Pipeline|Duration"
msgstr ""
msgid "Pipeline|Failed" msgid "Pipeline|Failed"
msgstr "" msgstr ""
@ -27743,6 +27752,9 @@ msgstr ""
msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}." msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."
msgstr "" msgstr ""
msgid "Preparing the report for the scan."
msgstr ""
msgid "Prev" msgid "Prev"
msgstr "" msgstr ""
@ -30798,6 +30810,12 @@ msgstr ""
msgid "Report abuse to admin" msgid "Report abuse to admin"
msgstr "" msgstr ""
msgid "Report couldn't be prepared."
msgstr ""
msgid "Report for the scan has been removed from the database."
msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}" msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr "" msgstr ""
@ -36831,6 +36849,9 @@ msgstr ""
msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable." msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable."
msgstr "" msgstr ""
msgid "The related CI build failed."
msgstr ""
msgid "The remote mirror URL is invalid." msgid "The remote mirror URL is invalid."
msgstr "" msgstr ""
@ -36840,6 +36861,12 @@ msgstr ""
msgid "The remote repository is being updated..." msgid "The remote repository is being updated..."
msgstr "" msgstr ""
msgid "The report artifact provided by the CI build couldn't be parsed."
msgstr ""
msgid "The report has been successfully prepared."
msgstr ""
msgid "The repository can be committed to, and issues, comments and other entities can be created." msgid "The repository can be committed to, and issues, comments and other entities can be created."
msgstr "" msgstr ""
@ -36861,6 +36888,9 @@ msgstr ""
msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)." msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)."
msgstr "" msgstr ""
msgid "The scan has been created."
msgstr ""
msgid "The snippet can be accessed without any authentication." msgid "The snippet can be accessed without any authentication."
msgstr "" msgstr ""
@ -37314,7 +37344,7 @@ msgstr ""
msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment." msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment."
msgstr "" msgstr ""
msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}." msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}."
msgstr "" msgstr ""
msgid "This %{noteableTypeText} is locked." msgid "This %{noteableTypeText} is locked."

View File

@ -24,6 +24,7 @@ module QA
view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
element :expand_pipeline_button element :expand_pipeline_button
element :child_pipeline element :child_pipeline
element :linked_pipeline_body
end end
view 'app/assets/javascripts/reports/components/report_section.vue' do view 'app/assets/javascripts/reports/components/report_section.vue' do
@ -93,7 +94,7 @@ module QA
end end
def find_child_pipeline_by_title(title) def find_child_pipeline_by_title(title)
child_pipelines.find { |pipeline| pipeline[:title].include?(title) } find_element(:child_pipeline, text: title)
end end
def expand_child_pipeline(title: nil) def expand_child_pipeline(title: nil)

View File

@ -1,104 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner do
describe 'Pass dotenv variables to downstream via bridge' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:upstream_var) { Faker::Alphanumeric.alphanumeric(8) }
let(:group) { Resource::Group.fabricate_via_api! }
let(:upstream_project) do
Resource::Project.fabricate_via_api! do |project|
project.group = group
project.name = 'upstream-project-with-bridge'
end
end
let(:downstream_project) do
Resource::Project.fabricate_via_api! do |project|
project.group = group
project.name = 'downstream-project-with-bridge'
end
end
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.name = executor
runner.tags = [executor]
runner.token = group.reload!.runners_token
end
end
before do
Flow::Login.sign_in
add_ci_file(downstream_project, downstream_ci_file)
add_ci_file(upstream_project, upstream_ci_file)
upstream_project.visit!
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'succeeded')
end
after do
runner.remove_via_api!
[upstream_project, downstream_project].each(&:remove_via_api!)
end
it 'runs the pipeline with composed config', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348088' do
Page::Project::Pipeline::Show.perform do |parent_pipeline|
Support::Waiter.wait_until { parent_pipeline.has_child_pipeline? }
parent_pipeline.expand_child_pipeline
parent_pipeline.click_job('downstream_test')
end
Page::Project::Job::Show.perform do |show|
expect(show).to have_passed(timeout: 360)
expect(show.output).to have_content(upstream_var)
end
end
private
def add_ci_file(project, file)
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add config file'
commit.add_files([file])
end
end
def upstream_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
build:
stage: build
tags: ["#{executor}"]
script:
- echo "DYNAMIC_ENVIRONMENT_VAR=#{upstream_var}" >> variables.env
artifacts:
reports:
dotenv: variables.env
trigger:
stage: deploy
variables:
PASSED_MY_VAR: $DYNAMIC_ENVIRONMENT_VAR
trigger: #{downstream_project.full_path}
YAML
}
end
def downstream_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
downstream_test:
stage: test
tags: ["#{executor}"]
script:
- echo $PASSED_MY_VAR
YAML
}
end
end
end
end

View File

@ -56,7 +56,7 @@ module QA
end end
Page::Project::Job::Show.perform do |show| Page::Project::Job::Show.perform do |show|
expect(show).to have_passed(timeout: 360) expect(show).to have_passed(timeout: 800)
end end
end end
end end

View File

@ -125,7 +125,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
before do before do
stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false) stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false)
stub_feature_flags(rearrange_pipelines_table: false)
end end
it 'creates a pipeline in the parent project when user proceeds with the warning' do it 'creates a pipeline in the parent project when user proceeds with the warning' do

View File

@ -161,7 +161,7 @@ RSpec.describe 'Pipelines', :js do
end end
end end
context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned off' do context 'when pipeline is detached merge request pipeline' do
let(:merge_request) do let(:merge_request) do
create(:merge_request, create(:merge_request,
:with_detached_merge_request_pipeline, :with_detached_merge_request_pipeline,
@ -174,52 +174,6 @@ RSpec.describe 'Pipelines', :js do
let(:target_project) { project } let(:target_project) { project }
before do before do
stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(source_project)
end
shared_examples_for 'detached merge request pipeline' do
it 'shows pipeline information without pipeline ref', :sidekiq_might_not_need_inline do
within '.pipeline-tags' do
expect(page).to have_content(expected_detached_mr_tag)
end
within '.branch-commit' do
expect(page).to have_link(merge_request.iid,
href: project_merge_request_path(project, merge_request))
end
within '.branch-commit' do
expect(page).not_to have_link(pipeline.ref)
end
end
end
it_behaves_like 'detached merge request pipeline'
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
it_behaves_like 'detached merge request pipeline'
end
end
context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned on' do
let(:merge_request) do
create(:merge_request,
:with_detached_merge_request_pipeline,
source_project: source_project,
target_project: target_project)
end
let!(:pipeline) { merge_request.all_pipelines.first }
let(:source_project) { project }
let(:target_project) { project }
before do
stub_feature_flags(rearrange_pipelines_table: true)
visit project_pipelines_path(source_project) visit project_pipelines_path(source_project)
end end
@ -245,7 +199,7 @@ RSpec.describe 'Pipelines', :js do
end end
end end
context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned off' do context 'when pipeline is merge request pipeline' do
let(:merge_request) do let(:merge_request) do
create(:merge_request, create(:merge_request,
:with_merge_request_pipeline, :with_merge_request_pipeline,
@ -259,53 +213,6 @@ RSpec.describe 'Pipelines', :js do
let(:target_project) { project } let(:target_project) { project }
before do before do
stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(source_project)
end
shared_examples_for 'Correct merge request pipeline information' do
it 'does not show detached tag for the pipeline, and shows the link of the merge request, and does not show the ref of the pipeline', :sidekiq_might_not_need_inline do
within '.pipeline-tags' do
expect(page).not_to have_content(expected_detached_mr_tag)
end
within '.branch-commit' do
expect(page).to have_link(merge_request.iid,
href: project_merge_request_path(project, merge_request))
end
within '.branch-commit' do
expect(page).not_to have_link(pipeline.ref)
end
end
end
it_behaves_like 'Correct merge request pipeline information'
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
it_behaves_like 'Correct merge request pipeline information'
end
end
context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned on' do
let(:merge_request) do
create(:merge_request,
:with_merge_request_pipeline,
source_project: source_project,
target_project: target_project,
merge_sha: target_project.commit.sha)
end
let!(:pipeline) { merge_request.all_pipelines.first }
let(:source_project) { project }
let(:target_project) { project }
before do
stub_feature_flags(rearrange_pipelines_table: true)
visit project_pipelines_path(source_project) visit project_pipelines_path(source_project)
end end
@ -676,28 +583,6 @@ RSpec.describe 'Pipelines', :js do
context 'with pipeline key selection' do context 'with pipeline key selection' do
before do before do
stub_feature_flags(rearrange_pipelines_table: false)
visit project_pipelines_path(project)
wait_for_requests
end
it 'changes the Pipeline ID column for Pipeline IID' do
page.find('[data-testid="pipeline-key-dropdown"]').click
within '.gl-new-dropdown-contents' do
dropdown_options = page.find_all '.gl-new-dropdown-item'
dropdown_options[1].click
end
expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline IID'
expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
end
end
context 'with pipeline key selection and rearrange_pipelines_table ff on' do
before do
stub_feature_flags(rearrange_pipelines_table: true)
visit project_pipelines_path(project) visit project_pipelines_path(project)
wait_for_requests wait_for_requests
end end

View File

@ -93,5 +93,38 @@ describe('DirtySubmitForm', () => {
expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length); expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length);
}); });
describe('when inputs listener is added', () => {
it('calls listener when changes are made to an input', () => {
const { form, input } = createForm();
const inputsListener = jest.fn();
const dirtySubmitForm = new DirtySubmitForm(form);
dirtySubmitForm.addInputsListener(inputsListener);
setInputValue(input, 'new value');
jest.runOnlyPendingTimers();
expect(inputsListener).toHaveBeenCalledTimes(1);
});
describe('when inputs listener is removed', () => {
it('does not call listener when changes are made to an input', () => {
const { form, input } = createForm();
const inputsListener = jest.fn();
const dirtySubmitForm = new DirtySubmitForm(form);
dirtySubmitForm.addInputsListener(inputsListener);
dirtySubmitForm.removeInputsListener(inputsListener);
setInputValue(input, 'new value');
jest.runOnlyPendingTimers();
expect(inputsListener).not.toHaveBeenCalled();
});
});
});
}); });
}); });

View File

@ -1,4 +1,4 @@
import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui'; import { GlIntersectionObserver, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
@ -19,6 +19,7 @@ describe('Jobs app', () => {
let resolverSpy; let resolverSpy;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findJobsTable = () => wrapper.findComponent(JobsTable); const findJobsTable = () => wrapper.findComponent(JobsTable);
const triggerInfiniteScroll = () => const triggerInfiniteScroll = () =>
@ -48,7 +49,29 @@ describe('Jobs app', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('displays the loading state', () => { describe('loading spinner', () => {
beforeEach(async () => {
createComponent(resolverSpy);
await waitForPromises();
triggerInfiniteScroll();
});
it('displays loading spinner when fetching more jobs', () => {
expect(findLoadingSpinner().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
});
it('hides loading spinner after jobs have been fetched', async () => {
await waitForPromises();
expect(findLoadingSpinner().exists()).toBe(false);
expect(findSkeletonLoader().exists()).toBe(false);
});
});
it('displays the skeleton loader', () => {
createComponent(resolverSpy); createComponent(resolverSpy);
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
@ -91,7 +114,7 @@ describe('Jobs app', () => {
}); });
}); });
it('does not display main loading state again after fetchMore', async () => { it('does not display skeleton loader again after fetchMore', async () => {
createComponent(resolverSpy); createComponent(resolverSpy);
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);

View File

@ -66,7 +66,6 @@ describe('graph component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('with data', () => { describe('with data', () => {

View File

@ -1,5 +1,5 @@
import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, GlPopover } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants'; import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
@ -9,15 +9,20 @@ import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => { describe('Linked pipeline', () => {
let wrapper; let wrapper;
const defaultProps = {
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
isMultiProjectVizAvailable: true,
};
const downstreamProps = { const downstreamProps = {
pipeline: { pipeline: {
...mockPipeline, ...mockPipeline,
multiproject: false, multiproject: false,
}, },
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
}; };
const upstreamProps = { const upstreamProps = {
@ -27,21 +32,29 @@ describe('Linked pipeline', () => {
}; };
const findButton = () => wrapper.find(GlButton); const findButton = () => wrapper.find(GlButton);
const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]'); const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); const findLinkedPipeline = () => wrapper.findByTestId('linkedPipeline');
const findLinkedPipelineBody = () => wrapper.findByTestId('linkedPipelineBody');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]'); const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]'); const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findPopover = () => wrapper.find(GlPopover);
const createWrapper = (propsData, data = []) => { const createWrapper = (propsData, data = []) => {
wrapper = mount(LinkedPipelineComponent, { wrapper = mountExtended(LinkedPipelineComponent, {
propsData, propsData: {
...defaultProps,
...propsData,
},
data() { data() {
return { return {
...data, ...data,
}; };
}, },
provide: {
multiProjectHelpPath: '/ci/pipelines/multi-project-pipelines',
},
}); });
}; };
@ -92,7 +105,7 @@ describe('Linked pipeline', () => {
}); });
it('should render the tooltip text as the title attribute', () => { it('should render the tooltip text as the title attribute', () => {
const titleAttr = findLinkedPipeline().attributes('title'); const titleAttr = findLinkedPipelineBody().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name); expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.status.label); expect(titleAttr).toContain(mockPipeline.status.label);
@ -168,10 +181,6 @@ describe('Linked pipeline', () => {
describe('when isLoading is true', () => { describe('when isLoading is true', () => {
const props = { const props = {
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: true, isLoading: true,
}; };
@ -184,19 +193,43 @@ describe('Linked pipeline', () => {
}); });
}); });
describe('on click/hover', () => { describe('when the user does not have access to the multi-project pipeline viz feature', () => {
const props = {
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
beforeEach(() => { beforeEach(() => {
const props = { isMultiProjectVizAvailable: false };
createWrapper(props); createWrapper(props);
}); });
it('the multi-project expand button is disabled', () => {
expect(findExpandButton().props('disabled')).toBe(true);
});
it('it adds the popover text inside the DOM', () => {
expect(findPopover().exists()).toBe(true);
expect(findPopover().text()).toContain(
'Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines.',
);
});
});
describe('when the user has access to the multi-project pipeline viz feature', () => {
beforeEach(() => {
createWrapper();
});
it('the multi-project expand button is enabled', () => {
expect(findExpandButton().props('disabled')).toBe(false);
});
it('does not add the popover text inside the DOM', () => {
expect(findPopover().exists()).toBe(false);
});
});
describe('on click/hover', () => {
beforeEach(() => {
createWrapper();
});
it('emits `pipelineClicked` event', () => { it('emits `pipelineClicked` event', () => {
jest.spyOn(wrapper.vm, '$emit'); jest.spyOn(wrapper.vm, '$emit');
findButton().trigger('click'); findButton().trigger('click');

View File

@ -26,6 +26,7 @@ const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
describe('Linked Pipelines Column', () => { describe('Linked Pipelines Column', () => {
const defaultProps = { const defaultProps = {
columnTitle: 'Downstream', columnTitle: 'Downstream',
isMultiProjectVizAvailable: true,
linkedPipelines: processedPipeline.downstream, linkedPipelines: processedPipeline.downstream,
showLinks: false, showLinks: false,
type: DOWNSTREAM, type: DOWNSTREAM,
@ -51,6 +52,9 @@ describe('Linked Pipelines Column', () => {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
provide: {
multiProjectHelpPath: 'ci/pipelines/multi-project-pipeline',
},
}); });
}; };
@ -67,7 +71,6 @@ describe('Linked Pipelines Column', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('it renders correctly', () => { describe('it renders correctly', () => {

View File

@ -13,6 +13,15 @@ export const mockPipelineResponse = {
usesNeeds: true, usesNeeds: true,
downstream: null, downstream: null,
upstream: null, upstream: null,
user: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
namespace: {
__typename: 'Namespace',
id: 'gid://gitlab/Namespaces::UserNamespace/1',
crossProjectPipelineAvailable: true,
},
},
userPermissions: { userPermissions: {
__typename: 'PipelinePermissions', __typename: 'PipelinePermissions',
updatePipeline: true, updatePipeline: true,
@ -780,6 +789,15 @@ export const wrappedPipelineReturn = {
id: 'gid://gitlab/Ci::Pipeline/175', id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38', iid: '38',
complete: true, complete: true,
user: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
namespace: {
__typename: 'Namespace',
id: 'gid://gitlab/Namespaces::UserNamespace/1',
crossProjectPipelineAvailable: true,
},
},
usesNeeds: true, usesNeeds: true,
userPermissions: { userPermissions: {
__typename: 'PipelinePermissions', __typename: 'PipelinePermissions',

View File

@ -30,14 +30,11 @@ describe('Pipeline Url Component', () => {
const defaultProps = mockPipeline(projectPath); const defaultProps = mockPipeline(projectPath);
const createComponent = (props, rearrangePipelinesTable = false) => { const createComponent = (props) => {
wrapper = shallowMountExtended(PipelineUrlComponent, { wrapper = shallowMountExtended(PipelineUrlComponent, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
provide: { provide: {
targetProjectFullPath: projectPath, targetProjectFullPath: projectPath,
glFeatures: {
rearrangePipelinesTable,
},
}, },
}); });
}; };
@ -47,7 +44,6 @@ describe('Pipeline Url Component', () => {
wrapper = null; wrapper = null;
}); });
describe('with the rearrangePipelinesTable feature flag turned off', () => {
it('should render pipeline url table cell', () => { it('should render pipeline url table cell', () => {
createComponent(); createComponent();
@ -194,15 +190,6 @@ describe('Pipeline Url Component', () => {
expect(findTrainTag().exists()).toBe(false); expect(findTrainTag().exists()).toBe(false);
}); });
it('should not render the commit wrapper and commit-short-sha', () => {
createComponent();
expect(findCommitTitleContainer().exists()).toBe(false);
expect(findCommitShortSha().exists()).toBe(false);
});
});
describe('with the rearrangePipelinesTable feature flag turned on', () => {
it('should render the commit title, commit reference and commit-short-sha', () => { it('should render the commit title, commit reference and commit-short-sha', () => {
createComponent({}, true); createComponent({}, true);
@ -224,13 +211,9 @@ describe('Pipeline Url Component', () => {
${mockPipelineTag()} | ${'Tag'} ${mockPipelineTag()} | ${'Tag'}
${mockPipelineBranch()} | ${'Branch'} ${mockPipelineBranch()} | ${'Branch'}
${mockPipeline()} | ${'Merge Request'} ${mockPipeline()} | ${'Merge Request'}
`( `('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => {
'should render tooltip $expectedTitle for commit icon type',
({ pipeline, expectedTitle }) => {
createComponent(pipeline, true); createComponent(pipeline, true);
expect(findCommitIconType().attributes('title')).toBe(expectedTitle); expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
},
);
}); });
}); });

View File

@ -17,7 +17,6 @@ import {
import eventHub from '~/pipelines/event_hub'; import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
jest.mock('~/pipelines/event_hub'); jest.mock('~/pipelines/event_hub');
@ -37,18 +36,13 @@ describe('Pipelines Table', () => {
return pipelines.find((p) => p.user !== null && p.commit !== null); return pipelines.find((p) => p.user !== null && p.commit !== null);
}; };
const createComponent = (props = {}, rearrangePipelinesTable = false) => { const createComponent = (props = {}) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(PipelinesTable, { mount(PipelinesTable, {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
provide: {
glFeatures: {
rearrangePipelinesTable,
},
},
}), }),
); );
}; };
@ -57,7 +51,6 @@ describe('Pipelines Table', () => {
const findStatusBadge = () => wrapper.findComponent(CiBadge); const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago); const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations); const findActions = () => wrapper.findComponent(PipelineOperations);
@ -65,10 +58,7 @@ describe('Pipelines Table', () => {
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findStatusTh = () => wrapper.findByTestId('status-th'); const findStatusTh = () => wrapper.findByTestId('status-th');
const findPipelineTh = () => wrapper.findByTestId('pipeline-th'); const findPipelineTh = () => wrapper.findByTestId('pipeline-th');
const findTriggererTh = () => wrapper.findByTestId('triggerer-th');
const findCommitTh = () => wrapper.findByTestId('commit-th');
const findStagesTh = () => wrapper.findByTestId('stages-th'); const findStagesTh = () => wrapper.findByTestId('stages-th');
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
const findActionsTh = () => wrapper.findByTestId('actions-th'); const findActionsTh = () => wrapper.findByTestId('actions-th');
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
@ -82,7 +72,7 @@ describe('Pipelines Table', () => {
wrapper = null; wrapper = null;
}); });
describe('Pipelines Table with rearrangePipelinesTable feature flag turned off', () => { describe('Pipelines Table', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' }); createComponent({ pipelines: [pipeline], viewType: 'root' });
}); });
@ -93,11 +83,8 @@ describe('Pipelines Table', () => {
it('should render table head with correct columns', () => { it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status'); expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline ID'); expect(findPipelineTh().text()).toBe('Pipeline');
expect(findTriggererTh().text()).toBe('Triggerer');
expect(findCommitTh().text()).toBe('Commit');
expect(findStagesTh().text()).toBe('Stages'); expect(findStagesTh().text()).toBe('Stages');
expect(findTimeAgoTh().text()).toBe('Duration');
expect(findActionsTh().text()).toBe('Actions'); expect(findActionsTh().text()).toBe('Actions');
}); });
@ -125,27 +112,6 @@ describe('Pipelines Table', () => {
}); });
}); });
describe('triggerer cell', () => {
it('should render the pipeline triggerer', () => {
expect(findTriggerer().exists()).toBe(true);
});
});
describe('commit cell', () => {
it('should render commit information', () => {
expect(findCommit().exists()).toBe(true);
});
it('should display and link to commit', () => {
expect(findCommit().text()).toContain(pipeline.commit.short_id);
expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path);
});
it('should display the commit author', () => {
expect(findCommit().props('author')).toEqual(pipeline.commit.author);
});
});
describe('stages cell', () => { describe('stages cell', () => {
it('should render a pipeline mini graph', () => { it('should render a pipeline mini graph', () => {
expect(findPipelineMiniGraph().exists()).toBe(true); expect(findPipelineMiniGraph().exists()).toBe(true);
@ -163,7 +129,7 @@ describe('Pipelines Table', () => {
pipeline = createMockPipeline(); pipeline = createMockPipeline();
pipeline.details.stages = null; pipeline.details.stages = null;
createComponent({ pipelines: [pipeline] }, true); createComponent({ pipelines: [pipeline] });
}); });
it('stages are not rendered', () => { it('stages are not rendered', () => {
@ -176,7 +142,7 @@ describe('Pipelines Table', () => {
}); });
it('when update graph dropdown is set, should update graph dropdown', () => { it('when update graph dropdown is set, should update graph dropdown', () => {
createComponent({ pipelines: [pipeline], updateGraphDropdown: true }, true); createComponent({ pipelines: [pipeline], updateGraphDropdown: true });
expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true); expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true);
}); });
@ -207,30 +173,11 @@ describe('Pipelines Table', () => {
expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
}); });
}); });
});
describe('Pipelines Table with rearrangePipelinesTable feature flag turned on', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' }, true);
});
it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline');
expect(findStagesTh().text()).toBe('Stages');
expect(findActionsTh().text()).toBe('Actions');
});
describe('triggerer cell', () => { describe('triggerer cell', () => {
it('should render the pipeline triggerer', () => { it('should render the pipeline triggerer', () => {
expect(findTriggerer().exists()).toBe(true); expect(findTriggerer().exists()).toBe(true);
}); });
}); });
describe('commit cell', () => {
it('should not render commit information', () => {
expect(findCommit().exists()).toBe(false);
});
});
}); });
}); });

View File

@ -34,21 +34,19 @@ exports[`Issue Warning Component when noteable is locked and confidential render
<span> <span>
<span> <span>
This issue is This issue is
<a <gl-link-stub
href="" href=""
rel="noopener noreferrer"
target="_blank" target="_blank"
> >
confidential confidential
</a> </gl-link-stub>
and and
<a <gl-link-stub
href="" href=""
rel="noopener noreferrer"
target="_blank" target="_blank"
> >
locked locked
</a> </gl-link-stub>
. .
</span> </span>

View File

@ -1,4 +1,4 @@
import { GlIcon } from '@gitlab/ui'; import { GlIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
@ -16,6 +16,9 @@ describe('Issue Warning Component', () => {
propsData: { propsData: {
...props, ...props,
}, },
stubs: {
GlSprintf,
},
}); });
afterEach(() => { afterEach(() => {

View File

@ -0,0 +1,51 @@
import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlIcon } from '@gitlab/ui';
import TierBadge from '~/vue_shared/components/tier_badge.vue';
describe('Tier badge component', () => {
let wrapper;
const createComponent = (props) =>
shallowMount(TierBadge, {
propsData: {
...props,
},
});
const findBadge = () => wrapper.findComponent(GlBadge);
const findTierText = () => findBadge().text();
const findIcon = () => wrapper.findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
});
describe('tiers name', () => {
it.each`
tier | tierText
${'free'} | ${'Free'}
${'premium'} | ${'Premium'}
${'ultimate'} | ${'Ultimate'}
`(
'shows $tierText text in the badge and the license icon when $tier prop is passed',
({ tier, tierText }) => {
wrapper = createComponent({ tier });
expect(findTierText()).toBe(tierText);
expect(findIcon().exists()).toBe(true);
expect(findIcon().props().name).toBe('license');
},
);
});
describe('badge size', () => {
const newSize = 'lg';
beforeEach(() => {
wrapper = createComponent({ tier: 'free', size: newSize });
});
it('passes down the size prop to the GlBadge component', () => {
expect(findBadge().props().size).toBe(newSize);
});
});
});

View File

@ -102,9 +102,9 @@ RSpec.describe Types::BaseEnum do
it 'sets the values defined by the declarative enum' do it 'sets the values defined by the declarative enum' do
set_declarative_enum set_declarative_enum
expect(enum_type.values.keys).to eq(['FOO']) expect(enum_type.values.keys).to contain_exactly('FOO')
expect(enum_type.values.values.map(&:description)).to eq(['description of foo']) expect(enum_type.values.values.map(&:description)).to contain_exactly('description of foo')
expect(enum_type.values.values.map(&:value)).to eq([0]) expect(enum_type.values.values.map(&:value)).to contain_exactly('foo')
end end
end end
end end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::PipelineHelper do
describe '#js_pipeline_details_data' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
subject(:pipeline_details_data) { helper.js_pipeline_details_data(project, pipeline) }
it 'returns pipeline details data' do
expect(pipeline_details_data).to eq({
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
multi_project_help_path: help_page_path('ci/pipelines/multi_project_pipelines.md', anchor: 'multi-project-pipeline-visualization'),
pipeline_iid: pipeline.iid,
pipeline_project_path: project.full_path
})
end
end
end

View File

@ -168,24 +168,100 @@ RSpec.describe ContainerRegistry::Client do
expect(subject).to eq('Blob') expect(subject).to eq('Blob')
end end
it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do context 'with a 307 redirect' do
let(:redirect_location) { 'http://redirected' }
before do
stub_request(method, url) stub_request(method, url)
.with(headers: blob_headers) .with(headers: blob_headers)
.to_return(status: 307, body: '', headers: { Location: 'http://redirected' }) .to_return(status: 307, body: '', headers: { Location: redirect_location })
# We should probably use hash_excluding here, but that requires an update to WebMock: # We should probably use hash_excluding here, but that requires an update to WebMock:
# https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb
stub_request(:get, "http://redirected/") stub_request(:get, redirect_location)
.with(headers: redirect_header) do |request| .with(headers: redirect_header) do |request|
!request.headers.include?('Authorization') !request.headers.include?('Authorization')
end end
.to_return(status: 200, body: "Successfully redirected") .to_return(status: 200, body: "Successfully redirected")
end
shared_examples 'handling redirects' do
it 'follows the redirect' do
expect(Faraday::Utils).not_to receive(:escape).with('signature=')
expect_new_faraday
expect(subject).to eq('Successfully redirected')
end
end
it_behaves_like 'handling redirects'
context 'with a redirect location with params ending with =' do
let(:redirect_location) { 'http://redirect?foo=bar&test=signature=' }
it_behaves_like 'handling redirects'
context 'with container_registry_follow_redirects_middleware disabled' do
before do
stub_feature_flags(container_registry_follow_redirects_middleware: false)
end
it 'follows the redirect' do
expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original
expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original
expect(Faraday::Utils).to receive(:escape).with('test').and_call_original
expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original
expect_new_faraday(times: 2) expect_new_faraday(times: 2)
expect(subject).to eq('Successfully redirected') expect(subject).to eq('Successfully redirected')
end end
end
end
context 'with a redirect location with params ending with %3D' do
let(:redirect_location) { 'http://redirect?foo=bar&test=signature%3D' }
it_behaves_like 'handling redirects'
context 'with container_registry_follow_redirects_middleware disabled' do
before do
stub_feature_flags(container_registry_follow_redirects_middleware: false)
end
it 'follows the redirect' do
expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original
expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original
expect(Faraday::Utils).to receive(:escape).with('test').and_call_original
expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original
expect_new_faraday(times: 2)
expect(subject).to eq('Successfully redirected')
end
end
end
end
it_behaves_like 'handling timeouts' it_behaves_like 'handling timeouts'
# TODO Remove this context along with the
# container_registry_follow_redirects_middleware feature flag
# See https://gitlab.com/gitlab-org/gitlab/-/issues/353291
context 'faraday blob' do
subject { client.send(:faraday_blob) }
it 'has a follow redirects middleware' do
expect(subject.builder.handlers).to include(::FaradayMiddleware::FollowRedirects)
end
context 'with container_registry_follow_redirects_middleware is disabled' do
before do
stub_feature_flags(container_registry_follow_redirects_middleware: false)
end
it 'has not a follow redirects middleware' do
expect(subject.builder.handlers).not_to include(::FaradayMiddleware::FollowRedirects)
end
end
end
end end
describe '#upload_blob' do describe '#upload_blob' do

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigratePersonalNamespaceProjectMaintainerToOwner, :migration, schema: 20220208080921 do
let(:migration) { described_class.new }
let(:users_table) { table(:users) }
let(:members_table) { table(:members) }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:table_name) { 'members' }
let(:batch_column) { :id }
let(:sub_batch_size) { 10 }
let(:pause_ms) { 0 }
let(:owner_access) { 50 }
let(:maintainer_access) { 40 }
let(:developer_access) { 30 }
subject(:perform_migration) { migration.perform(1, 10, table_name, batch_column, sub_batch_size, pause_ms) }
before do
users_table.create!(id: 101, name: "user1", email: "user1@example.com", projects_limit: 5)
users_table.create!(id: 102, name: "user2", email: "user2@example.com", projects_limit: 5)
namespaces_table.create!(id: 201, name: 'user1s-namespace', path: 'user1s-namespace-path', type: 'User', owner_id: 101)
namespaces_table.create!(id: 202, name: 'user2s-namespace', path: 'user2s-namespace-path', type: 'User', owner_id: 102)
namespaces_table.create!(id: 203, name: 'group', path: 'group', type: 'Group')
namespaces_table.create!(id: 204, name: 'project-namespace', path: 'project-namespace-path', type: 'Project')
projects_table.create!(id: 301, name: 'user1-namespace-project', path: 'project-path-1', namespace_id: 201)
projects_table.create!(id: 302, name: 'user2-namespace-project', path: 'project-path-2', namespace_id: 202)
projects_table.create!(id: 303, name: 'user2s-namespace-project2', path: 'project-path-3', namespace_id: 202)
projects_table.create!(id: 304, name: 'group-project3', path: 'group-project-path-3', namespace_id: 203)
# user1 member of their own namespace project, maintainer access (change)
create_project_member(id: 1, user_id: 101, project_id: 301, level: maintainer_access)
# user2 member of their own namespace project, owner access (no change)
create_project_member(id: 2, user_id: 102, project_id: 302, level: owner_access)
# user1 member of user2's personal namespace project, maintainer access (no change)
create_project_member(id: 3, user_id: 101, project_id: 302, level: maintainer_access)
# user1 member of group project, maintainer access (no change)
create_project_member(id: 4, user_id: 101, project_id: 304, level: maintainer_access)
# user1 member of group, Maintainer role (no change)
create_group_member(id: 5, user_id: 101, group_id: 203, level: maintainer_access)
# user2 member of their own namespace project, maintainer access, but out of batch range (no change)
create_project_member(id: 601, user_id: 102, project_id: 303, level: maintainer_access)
end
it 'migrates MAINTAINER membership records for personal namespaces to OWNER', :aggregate_failures do
expect(members_table.where(access_level: owner_access).count).to eq 1
expect(members_table.where(access_level: maintainer_access).count).to eq 5
queries = ActiveRecord::QueryRecorder.new do
perform_migration
end
expect(queries.count).to eq(3)
expect(members_table.where(access_level: owner_access).pluck(:id)).to match_array([1, 2])
expect(members_table.where(access_level: maintainer_access).pluck(:id)).to match_array([3, 4, 5, 601])
end
it 'tracks timings of queries' do
expect(migration.batch_metrics.timings).to be_empty
expect { perform_migration }.to change { migration.batch_metrics.timings }
end
def create_group_member(id:, user_id:, group_id:, level:)
members_table.create!(id: id, user_id: user_id, source_id: group_id, access_level: level, source_type: "Namespace", type: "GroupMember", notification_level: 3)
end
def create_project_member(id:, user_id:, project_id:, level:)
members_table.create!(id: id, user_id: user_id, source_id: project_id, access_level: level, source_type: "Namespace", type: "ProjectMember", notification_level: 3)
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleMigratePersonalNamespaceProjectMaintainerToOwner do
let_it_be(:migration) { described_class::MIGRATION }
describe '#up' do
it 'schedules background jobs for each batch of members' do
migrate!
expect(migration).to have_scheduled_batched_migration(
table_name: :members,
column_name: :id,
interval: described_class::INTERVAL
)
end
end
end

View File

@ -4,40 +4,22 @@ require 'spec_helper'
RSpec.describe IssueLinks::DestroyService do RSpec.describe IssueLinks::DestroyService do
describe '#execute' do describe '#execute' do
let(:project) { create(:project_empty_repo) } let_it_be(:project) { create(:project_empty_repo, :private) }
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:issue_a) { create(:issue, project: project) }
let_it_be(:issue_b) { create(:issue, project: project) }
subject { described_class.new(issue_link, user).execute } let!(:issuable_link) { create(:issue_link, source: issue_a, target: issue_b) }
context 'when successfully removes an issue link' do subject { described_class.new(issuable_link, user).execute }
let(:issue_a) { create(:issue, project: project) }
let(:issue_b) { create(:issue, project: project) }
let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) } it_behaves_like 'a destroyable issuable link'
context 'when target is an incident' do
before do before do
project.add_reporter(user) project.add_reporter(user)
end end
it 'removes related issue' do
expect { subject }.to change(IssueLink, :count).from(1).to(0)
end
it 'creates notes' do
# Two-way notes creation
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.source, issue_link.target, user)
expect(SystemNoteService).to receive(:unrelate_issue)
.with(issue_link.target, issue_link.source, user)
subject
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
context 'target is an incident' do
let(:issue_b) { create(:incident, project: project) } let(:issue_b) { create(:incident, project: project) }
it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do
@ -45,25 +27,4 @@ RSpec.describe IssueLinks::DestroyService do
end end
end end
end end
context 'when failing to remove an issue link' do
let(:unauthorized_project) { create(:project) }
let(:issue_a) { create(:issue, project: project) }
let(:issue_b) { create(:issue, project: unauthorized_project) }
let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) }
it 'does not remove relation' do
expect { subject }.not_to change(IssueLink, :count).from(1)
end
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:unrelate_issue)
end
it 'returns error message' do
is_expected.to eq(message: 'No Issue Link found', status: :error, http_status: 404)
end
end
end
end end

View File

@ -117,7 +117,7 @@ RSpec.describe SystemNoteService do
end end
end end
describe '.unrelate_issue' do describe '.unrelate_issuable' do
let(:noteable_ref) { double } let(:noteable_ref) { double }
let(:noteable) { double } let(:noteable) { double }
@ -127,10 +127,10 @@ RSpec.describe SystemNoteService do
it 'calls IssuableService' do it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service| expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:unrelate_issue).with(noteable_ref) expect(service).to receive(:unrelate_issuable).with(noteable_ref)
end end
described_class.unrelate_issue(noteable, noteable_ref, double) described_class.unrelate_issuable(noteable, noteable_ref, double)
end end
end end

View File

@ -30,10 +30,10 @@ RSpec.describe ::SystemNotes::IssuablesService do
end end
end end
describe '#unrelate_issue' do describe '#unrelate_issuable' do
let(:noteable_ref) { create(:issue) } let(:noteable_ref) { create(:issue) }
subject { service.unrelate_issue(noteable_ref) } subject { service.unrelate_issuable(noteable_ref) }
it_behaves_like 'a system note' do it_behaves_like 'a system note' do
let(:action) { 'unrelate' } let(:action) { 'unrelate' }

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe WebHooks::LogExecutionService do RSpec.describe WebHooks::LogExecutionService do
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
using RSpec::Parameterized::TableSyntax
describe '#execute' do describe '#execute' do
around do |example| around do |example|
@ -34,11 +35,13 @@ RSpec.describe WebHooks::LogExecutionService do
expect(WebHookLog.recent.first).to have_attributes(data) expect(WebHookLog.recent.first).to have_attributes(data)
end end
context 'obtaining an exclusive lease' do
let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }
it 'updates failure state using a lease that ensures fresh state is written' do it 'updates failure state using a lease that ensures fresh state is written' do
service = described_class.new(hook: project_hook, log_data: data, response_category: :error) service = described_class.new(hook: project_hook, log_data: data, response_category: :error)
WebHook.find(project_hook.id).update!(backoff_count: 1) WebHook.find(project_hook.id).update!(backoff_count: 1)
lease_key = "web_hooks:update_hook_failure_state:#{project_hook.id}"
lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL) lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL)
expect(lease).to receive(:try_obtain) expect(lease).to receive(:try_obtain)
@ -46,6 +49,35 @@ RSpec.describe WebHooks::LogExecutionService do
expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2) expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2)
end end
context 'when a lease cannot be obtained' do
where(:response_category, :executable, :needs_updating) do
:ok | true | false
:ok | false | true
:failed | true | true
:failed | false | false
:error | true | true
:error | false | false
end
with_them do
subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }
before do
stub_exclusive_lease_taken(lease_key, timeout: described_class::LOCK_TTL)
allow(project_hook).to receive(:executable?).and_return(executable)
end
it 'raises an error if the hook needs to be updated' do
if needs_updating
expect { service.execute }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
else
expect { service.execute }.not_to raise_error
end
end
end
end
end
context 'when response_category is :ok' do context 'when response_category is :ok' do
it 'does not increment the failure count' do it 'does not increment the failure count' do
expect { service.execute }.not_to change(project_hook, :recent_failures) expect { service.execute }.not_to change(project_hook, :recent_failures)

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
shared_examples 'a destroyable issuable link' do
context 'when successfully removes an issuable link' do
before do
issuable_link.source.resource_parent.add_reporter(user)
issuable_link.target.resource_parent.add_reporter(user)
end
it 'removes related issue' do
expect { subject }.to change(issuable_link.class, :count).by(-1)
end
it 'creates notes' do
# Two-way notes creation
expect(SystemNoteService).to receive(:unrelate_issuable)
.with(issuable_link.source, issuable_link.target, user)
expect(SystemNoteService).to receive(:unrelate_issuable)
.with(issuable_link.target, issuable_link.source, user)
subject
end
it 'returns success message' do
is_expected.to eq(message: 'Relation was removed', status: :success)
end
end
context 'when failing to remove an issuable link' do
it 'does not remove relation' do
expect { subject }.not_to change(issuable_link.class, :count).from(1)
end
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:unrelate_issuable)
end
it 'returns error message' do
is_expected.to eq(message: "No #{issuable_link.class.model_name.human.titleize} found", status: :error, http_status: 404)
end
end
end