Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									73e15fde38
								
							
						
					
					
						commit
						698fe342b9
					
				|  | @ -1,11 +1,13 @@ | |||
| import $ from 'jquery'; | ||||
| import { memoize, throttle } from 'lodash'; | ||||
| import createEventHub from '~/helpers/event_hub_factory'; | ||||
| 
 | ||||
| class DirtySubmitForm { | ||||
|   constructor(form) { | ||||
|     this.form = form; | ||||
|     this.dirtyInputs = []; | ||||
|     this.isDisabled = true; | ||||
|     this.events = createEventHub(); | ||||
| 
 | ||||
|     this.init(); | ||||
|   } | ||||
|  | @ -36,11 +38,21 @@ class DirtySubmitForm { | |||
|     this.form.addEventListener('submit', (event) => this.formSubmit(event)); | ||||
|   } | ||||
| 
 | ||||
|   addInputsListener(callback) { | ||||
|     this.events.$on('input', callback); | ||||
|   } | ||||
| 
 | ||||
|   removeInputsListener(callback) { | ||||
|     this.events.$off('input', callback); | ||||
|   } | ||||
| 
 | ||||
|   updateDirtyInput(event) { | ||||
|     const { target } = event; | ||||
| 
 | ||||
|     if (!target.dataset.isDirtySubmitInput) return; | ||||
| 
 | ||||
|     this.events.$emit('input', event); | ||||
| 
 | ||||
|     this.updateDirtyInputs(target); | ||||
|     this.toggleSubmission(); | ||||
|   } | ||||
|  |  | |||
|  | @ -92,6 +92,9 @@ export default { | |||
|     hasUpstreamPipelines() { | ||||
|       return Boolean(this.pipeline?.upstream?.length > 0); | ||||
|     }, | ||||
|     isMultiProjectVizAvailable() { | ||||
|       return Boolean(this.pipeline?.user?.namespace?.crossProjectPipelineAvailable); | ||||
|     }, | ||||
|     isStageView() { | ||||
|       return this.viewType === STAGE_VIEW; | ||||
|     }, | ||||
|  | @ -178,6 +181,7 @@ export default { | |||
|           <linked-pipelines-column | ||||
|             v-if="showUpstreamPipelines" | ||||
|             :config-paths="configPaths" | ||||
|             :is-multi-project-viz-available="isMultiProjectVizAvailable" | ||||
|             :linked-pipelines="upstreamPipelines" | ||||
|             :column-title="__('Upstream')" | ||||
|             :show-links="showJobLinks" | ||||
|  | @ -226,6 +230,7 @@ export default { | |||
|             v-if="showDownstreamPipelines" | ||||
|             class="gl-mr-6" | ||||
|             :config-paths="configPaths" | ||||
|             :is-multi-project-viz-available="isMultiProjectVizAvailable" | ||||
|             :linked-pipelines="downstreamPipelines" | ||||
|             :column-title="__('Downstream')" | ||||
|             :show-links="showJobLinks" | ||||
|  |  | |||
|  | @ -1,12 +1,29 @@ | |||
| <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 { __, sprintf } from '~/locale'; | ||||
| import { __, s__, sprintf } from '~/locale'; | ||||
| import CiStatus from '~/vue_shared/components/ci_icon.vue'; | ||||
| import { reportToSentry } from '../../utils'; | ||||
| import { DOWNSTREAM, UPSTREAM } from './constants'; | ||||
| 
 | ||||
| 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: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|  | @ -16,7 +33,11 @@ export default { | |||
|     GlButton, | ||||
|     GlLink, | ||||
|     GlLoadingIcon, | ||||
|     GlPopover, | ||||
|     GlSprintf, | ||||
|     TierBadge, | ||||
|   }, | ||||
|   inject: ['multiProjectHelpPath'], | ||||
|   props: { | ||||
|     columnTitle: { | ||||
|       type: String, | ||||
|  | @ -26,6 +47,10 @@ export default { | |||
|       type: Boolean, | ||||
|       required: true, | ||||
|     }, | ||||
|     isMultiProjectVizAvailable: { | ||||
|       type: Boolean, | ||||
|       required: true, | ||||
|     }, | ||||
|     isLoading: { | ||||
|       type: Boolean, | ||||
|       required: true, | ||||
|  | @ -90,6 +115,9 @@ export default { | |||
|     pipelineStatus() { | ||||
|       return this.pipeline.status; | ||||
|     }, | ||||
|     popoverContainerId() { | ||||
|       return `popoverContainer-${this.pipeline.id}`; | ||||
|     }, | ||||
|     projectName() { | ||||
|       return this.pipeline.project.name; | ||||
|     }, | ||||
|  | @ -128,16 +156,21 @@ export default { | |||
| 
 | ||||
| <template> | ||||
|   <div | ||||
|     ref="linkedPipeline" | ||||
|     v-gl-tooltip | ||||
|     class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1" | ||||
|     :class="flexDirection" | ||||
|     :title="tooltipText" | ||||
|     data-qa-selector="child_pipeline" | ||||
|     data-testid="linkedPipeline" | ||||
|     @mouseover="onDownstreamHovered" | ||||
|     @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"> | ||||
|         <ci-status | ||||
|           v-if="!pipelineIsLoading" | ||||
|  | @ -163,17 +196,38 @@ export default { | |||
|         </gl-badge> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="gl-display-flex"> | ||||
|     <div :id="popoverContainerId" class="gl-display-flex"> | ||||
|       <gl-button | ||||
|         :id="buttonId" | ||||
|         class="gl-shadow-none! gl-rounded-0!" | ||||
|         :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`" | ||||
|         :icon="expandedIcon" | ||||
|         :aria-label="__('Expand pipeline')" | ||||
|         :disabled="!isMultiProjectVizAvailable" | ||||
|         data-testid="expand-pipeline-button" | ||||
|         data-qa-selector="expand_pipeline_button" | ||||
|         @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> | ||||
| </template> | ||||
|  |  | |||
|  | @ -28,6 +28,10 @@ export default { | |||
|       required: true, | ||||
|       validator: validateConfigPaths, | ||||
|     }, | ||||
|     isMultiProjectVizAvailable: { | ||||
|       type: Boolean, | ||||
|       required: true, | ||||
|     }, | ||||
|     linkedPipelines: { | ||||
|       type: Array, | ||||
|       required: true, | ||||
|  | @ -208,6 +212,7 @@ export default { | |||
|           <linked-pipeline | ||||
|             class="gl-display-inline-block" | ||||
|             :is-loading="isLoadingPipeline(pipeline.id)" | ||||
|             :is-multi-project-viz-available="isMultiProjectVizAvailable" | ||||
|             :pipeline="pipeline" | ||||
|             :column-title="columnTitle" | ||||
|             :type="type" | ||||
|  |  | |||
|  | @ -60,6 +60,15 @@ export default { | |||
|         iid: this.pipelineIid, | ||||
|       }; | ||||
|     }, | ||||
|     loading() { | ||||
|       return this.$apollo.queries.jobs.loading; | ||||
|     }, | ||||
|     showSkeletonLoader() { | ||||
|       return this.firstLoad && this.loading; | ||||
|     }, | ||||
|     showLoadingSpinner() { | ||||
|       return !this.firstLoad && this.loading; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     eventHub.$on('jobActionPerformed', this.handleJobAction); | ||||
|  | @ -69,7 +78,7 @@ export default { | |||
|   }, | ||||
|   methods: { | ||||
|     handleJobAction() { | ||||
|       this.firstLoad = true; | ||||
|       this.firstLoad = false; | ||||
| 
 | ||||
|       this.$apollo.queries.jobs.refetch(); | ||||
|     }, | ||||
|  | @ -98,7 +107,7 @@ export default { | |||
| 
 | ||||
| <template> | ||||
|   <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"> | ||||
|         <circle cx="748.031" 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" /> | ||||
| 
 | ||||
|     <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> | ||||
|   </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| <script> | ||||
| 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 glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
| import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; | ||||
| import { SCHEDULE_ORIGIN, ICONS } from '../../constants'; | ||||
| 
 | ||||
|  | @ -18,7 +17,6 @@ export default { | |||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagMixin()], | ||||
|   inject: { | ||||
|     targetProjectFullPath: { | ||||
|       default: '', | ||||
|  | @ -139,28 +137,14 @@ export default { | |||
|     commitTitle() { | ||||
|       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> | ||||
| <template> | ||||
|   <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"> | ||||
|       <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 | ||||
|             :href="commitUrl" | ||||
|             class="commit-row-message gl-text-gray-900" | ||||
|  | @ -214,16 +198,6 @@ export default { | |||
|       }}</gl-link> | ||||
|       <!--End of commit row--> | ||||
|     </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"> | ||||
|       <gl-badge | ||||
|         v-if="isScheduled" | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -3,7 +3,6 @@ import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.v | |||
| import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants'; | ||||
| import { CHILD_VIEW } from '~/pipelines/constants'; | ||||
| 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'; | ||||
| 
 | ||||
| export default { | ||||
|  | @ -12,7 +11,6 @@ export default { | |||
|     CiBadge, | ||||
|     PipelinesTimeago, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagsMixin()], | ||||
|   props: { | ||||
|     pipeline: { | ||||
|       type: Object, | ||||
|  | @ -44,9 +42,6 @@ export default { | |||
|     codeQualityBuildPath() { | ||||
|       return this.pipeline?.details?.code_quality_build_path; | ||||
|     }, | ||||
|     rearrangePipelinesTable() { | ||||
|       return this.glFeatures?.rearrangePipelinesTable; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | @ -61,7 +56,7 @@ export default { | |||
|       :icon-classes="'gl-vertical-align-middle!'" | ||||
|       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 | ||||
|       v-if="shouldRenderCodeQualityWalkthrough" | ||||
|       :step="codeQualityStep" | ||||
|  |  | |||
|  | @ -1,16 +1,13 @@ | |||
| <script> | ||||
| import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; | ||||
| import { s__, __ } from '~/locale'; | ||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
| import eventHub from '../../event_hub'; | ||||
| import PipelineMiniGraph from './pipeline_mini_graph.vue'; | ||||
| import PipelineOperations from './pipeline_operations.vue'; | ||||
| import PipelineStopModal from './pipeline_stop_modal.vue'; | ||||
| import PipelineTriggerer from './pipeline_triggerer.vue'; | ||||
| import PipelineUrl from './pipeline_url.vue'; | ||||
| import PipelinesCommit from './pipelines_commit.vue'; | ||||
| import PipelinesStatusBadge from './pipelines_status_badge.vue'; | ||||
| import PipelinesTimeago from './time_ago.vue'; | ||||
| 
 | ||||
| const DEFAULT_TD_CLASS = 'gl-p-5!'; | ||||
| const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; | ||||
|  | @ -22,19 +19,16 @@ export default { | |||
|     GlTableLite, | ||||
|     LinkedPipelinesMiniList: () => | ||||
|       import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), | ||||
|     PipelinesCommit, | ||||
|     PipelineMiniGraph, | ||||
|     PipelineOperations, | ||||
|     PipelinesStatusBadge, | ||||
|     PipelineStopModal, | ||||
|     PipelinesTimeago, | ||||
|     PipelineTriggerer, | ||||
|     PipelineUrl, | ||||
|   }, | ||||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   mixins: [glFeatureFlagMixin()], | ||||
|   props: { | ||||
|     pipelines: { | ||||
|       type: Array, | ||||
|  | @ -74,18 +68,16 @@ export default { | |||
|           key: 'status', | ||||
|           label: s__('Pipeline|Status'), | ||||
|           thClass: DEFAULT_TH_CLASSES, | ||||
|           columnClass: this.rearrangePipelinesTable ? 'gl-w-15p' : 'gl-w-10p', | ||||
|           columnClass: 'gl-w-15p', | ||||
|           tdClass: DEFAULT_TD_CLASS, | ||||
|           thAttr: { 'data-testid': 'status-th' }, | ||||
|         }, | ||||
|         { | ||||
|           key: 'pipeline', | ||||
|           label: this.rearrangePipelinesTable ? __('Pipeline') : this.pipelineKeyOption.label, | ||||
|           label: __('Pipeline'), | ||||
|           thClass: DEFAULT_TH_CLASSES, | ||||
|           tdClass: this.rearrangePipelinesTable | ||||
|             ? `${DEFAULT_TD_CLASS}` | ||||
|             : `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, | ||||
|           columnClass: this.rearrangePipelinesTable ? 'gl-w-30p' : 'gl-w-10p', | ||||
|           tdClass: `${DEFAULT_TD_CLASS}`, | ||||
|           columnClass: 'gl-w-30p', | ||||
|           thAttr: { 'data-testid': 'pipeline-th' }, | ||||
|         }, | ||||
|         { | ||||
|  | @ -96,14 +88,6 @@ export default { | |||
|           columnClass: 'gl-w-10p', | ||||
|           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', | ||||
|           label: s__('Pipeline|Stages'), | ||||
|  | @ -112,14 +96,6 @@ export default { | |||
|           columnClass: 'gl-w-quarter', | ||||
|           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', | ||||
|           thClass: DEFAULT_TH_CLASSES, | ||||
|  | @ -129,12 +105,7 @@ export default { | |||
|         }, | ||||
|       ]; | ||||
| 
 | ||||
|       return !this.rearrangePipelinesTable | ||||
|         ? fields | ||||
|         : fields.filter((field) => !['commit', 'timeago'].includes(field.key)); | ||||
|     }, | ||||
|     rearrangePipelinesTable() { | ||||
|       return this.glFeatures?.rearrangePipelinesTable; | ||||
|       return fields; | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|  | @ -200,10 +171,6 @@ export default { | |||
|         <pipeline-triggerer :pipeline="item" /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #cell(commit)="{ item }"> | ||||
|         <pipelines-commit :pipeline="item" :view-type="viewType" /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #cell(stages)="{ item }"> | ||||
|         <div class="stage-cell"> | ||||
|           <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> | ||||
|  | @ -229,10 +196,6 @@ export default { | |||
|         </div> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #cell(timeago)="{ item }"> | ||||
|         <pipelines-timeago :pipeline="item" /> | ||||
|       </template> | ||||
| 
 | ||||
|       <template #cell(actions)="{ item }"> | ||||
|         <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> | ||||
|       </template> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| <script> | ||||
| import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; | ||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; | ||||
| import timeagoMixin from '~/vue_shared/mixins/timeago'; | ||||
| 
 | ||||
| export default { | ||||
|  | @ -8,7 +7,7 @@ export default { | |||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   components: { GlIcon }, | ||||
|   mixins: [timeagoMixin, glFeatureFlagMixin()], | ||||
|   mixins: [timeagoMixin], | ||||
|   props: { | ||||
|     pipeline: { | ||||
|       type: Object, | ||||
|  | @ -54,14 +53,11 @@ export default { | |||
|     showSkipped() { | ||||
|       return !this.duration && !this.finishedTime && this.skipped; | ||||
|     }, | ||||
|     shouldDisplayAsBlock() { | ||||
|       return this.glFeatures?.rearrangePipelinesTable; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <div class="{ 'gl-display-block': shouldDisplayAsBlock }"> | ||||
|   <div class="gl-display-block"> | ||||
|     <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 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ Vue.use(VueApollo); | |||
| const createPipelinesDetailApp = ( | ||||
|   selector, | ||||
|   apolloProvider, | ||||
|   { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {}, | ||||
|   { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag, multiProjectHelpPath } = {}, | ||||
| ) => { | ||||
|   // eslint-disable-next-line no-new
 | ||||
|   new Vue({ | ||||
|  | @ -22,6 +22,7 @@ const createPipelinesDetailApp = ( | |||
|       pipelineProjectPath, | ||||
|       pipelineIid, | ||||
|       graphqlResourceEtag, | ||||
|       multiProjectHelpPath, | ||||
|     }, | ||||
|     errorCaptured(err, _vm, info) { | ||||
|       reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`); | ||||
|  |  | |||
|  | @ -1,12 +1,7 @@ | |||
| <script> | ||||
| import { GlLink, GlIcon } from '@gitlab/ui'; | ||||
| import { escape } from 'lodash'; | ||||
| import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui'; | ||||
| import { __, sprintf } from '~/locale'; | ||||
| 
 | ||||
| function buildDocsLinkStart(path) { | ||||
|   return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`; | ||||
| } | ||||
| 
 | ||||
| const NoteableTypeText = { | ||||
|   Issue: __('issue'), | ||||
|   Epic: __('epic'), | ||||
|  | @ -17,6 +12,7 @@ export default { | |||
|   components: { | ||||
|     GlIcon, | ||||
|     GlLink, | ||||
|     GlSprintf, | ||||
|   }, | ||||
|   props: { | ||||
|     isLocked: { | ||||
|  | @ -59,20 +55,6 @@ export default { | |||
|     noteableTypeText() { | ||||
|       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() { | ||||
|       return sprintf(__('This is a confidential %{noteableTypeText}.'), { | ||||
|         noteableTypeText: this.noteableTypeText, | ||||
|  | @ -91,9 +73,23 @@ export default { | |||
|     <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> | ||||
| 
 | ||||
|     <span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> | ||||
|       <span | ||||
|         v-html="confidentialAndLockedDiscussionText /* eslint-disable-line vue/no-v-html */" | ||||
|       ></span> | ||||
|       <span> | ||||
|         <gl-sprintf | ||||
|           :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.") | ||||
|       }} | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -5,6 +5,8 @@ class Admin::CohortsController < Admin::ApplicationController | |||
| 
 | ||||
|   feature_category :devops_reports | ||||
| 
 | ||||
|   urgency :low | ||||
| 
 | ||||
|   def index | ||||
|     @cohorts = load_cohorts | ||||
|     track_cohorts_visit | ||||
|  |  | |||
|  | @ -9,6 +9,8 @@ class Admin::DevOpsReportController < Admin::ApplicationController | |||
| 
 | ||||
|   feature_category :devops_reports | ||||
| 
 | ||||
|   urgency :low | ||||
| 
 | ||||
|   # rubocop: disable CodeReuse/ActiveRecord | ||||
|   def show | ||||
|     @metric = DevOpsReport::Metric.order(:created_at).last&.present | ||||
|  |  | |||
|  | @ -2,6 +2,8 @@ | |||
| class Admin::InstanceReviewController < Admin::ApplicationController | ||||
|   feature_category :devops_reports | ||||
| 
 | ||||
|   urgency :low | ||||
| 
 | ||||
|   def index | ||||
|     redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}") | ||||
|   end | ||||
|  |  | |||
|  | @ -7,6 +7,8 @@ class Admin::UsageTrendsController < Admin::ApplicationController | |||
| 
 | ||||
|   feature_category :devops_reports | ||||
| 
 | ||||
|   urgency :low | ||||
| 
 | ||||
|   def index | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -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(:refactor_mr_widgets_extensions, 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) | ||||
|     # Usage data feature flags | ||||
|     push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml) | ||||
|  |  | |||
|  | @ -16,9 +16,6 @@ class Projects::PipelinesController < Projects::ApplicationController | |||
|   before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] | ||||
|   before_action :authorize_update_pipeline!, only: [:retry, :cancel] | ||||
|   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 | ||||
|   before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? } | ||||
|  |  | |||
|  | @ -47,6 +47,15 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { | |||
|       id | ||||
|       iid | ||||
|       complete | ||||
|       user { | ||||
|         __typename | ||||
|         id | ||||
|         namespace { | ||||
|           __typename | ||||
|           id | ||||
|           crossProjectPipelineAvailable | ||||
|         } | ||||
|       } | ||||
|       usesNeeds | ||||
|       userPermissions { | ||||
|         updatePipeline | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ module Types | |||
|         description(enum_mod.description) if use_description | ||||
| 
 | ||||
|         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 | ||||
|       # rubocop: enable Graphql/Descriptions | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -12,21 +12,32 @@ module Boards | |||
|     end | ||||
| 
 | ||||
|     # rubocop: disable CodeReuse/ActiveRecord | ||||
|     def metadata | ||||
|       issuables = item_model.arel_table | ||||
|       keys = metadata_fields.keys | ||||
|     def metadata(required_fields = [:issue_count, :total_issue_weight]) | ||||
|       fields = metadata_fields(required_fields) | ||||
|       keys = fields.keys | ||||
|       # TODO: eliminate need for SQL literal fragment | ||||
|       columns = Arel.sql(metadata_fields.values_at(*keys).join(', ')) | ||||
|       results = item_model.where(id: init_collection.select(issuables[:id])).pluck(columns) | ||||
|       columns = Arel.sql(fields.values_at(*keys).join(', ')) | ||||
|       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 | ||||
|     # rubocop: enable CodeReuse/ActiveRecord | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def metadata_fields | ||||
|       { size: 'COUNT(*)' } | ||||
|     # override if needed | ||||
|     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 | ||||
| 
 | ||||
|     def order(items) | ||||
|  |  | |||
|  | @ -4,11 +4,13 @@ module IssuableLinks | |||
|   class DestroyService < BaseService | ||||
|     include IncidentManagement::UsageData | ||||
| 
 | ||||
|     attr_reader :link, :current_user | ||||
|     attr_reader :link, :current_user, :source, :target | ||||
| 
 | ||||
|     def initialize(link, user) | ||||
|       @link = link | ||||
|       @current_user = user | ||||
|       @source = link.source | ||||
|       @target = link.target | ||||
|     end | ||||
| 
 | ||||
|     def execute | ||||
|  | @ -22,6 +24,11 @@ module IssuableLinks | |||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def create_notes | ||||
|       SystemNoteService.unrelate_issuable(source, target, current_user) | ||||
|       SystemNoteService.unrelate_issuable(target, source, current_user) | ||||
|     end | ||||
| 
 | ||||
|     def after_destroy | ||||
|       create_notes | ||||
|       track_event | ||||
|  |  | |||
|  | @ -4,23 +4,10 @@ module IssueLinks | |||
|   class DestroyService < IssuableLinks::DestroyService | ||||
|     private | ||||
| 
 | ||||
|     def source | ||||
|       @source ||= link.source | ||||
|     end | ||||
| 
 | ||||
|     def target | ||||
|       @target ||= link.target | ||||
|     end | ||||
| 
 | ||||
|     def permission_to_remove_relation? | ||||
|       can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target) | ||||
|     end | ||||
| 
 | ||||
|     def create_notes | ||||
|       SystemNoteService.unrelate_issue(source, target, current_user) | ||||
|       SystemNoteService.unrelate_issue(target, source, current_user) | ||||
|     end | ||||
| 
 | ||||
|     def track_event | ||||
|       track_incident_action(current_user, target, :incident_unrelate) | ||||
|     end | ||||
|  |  | |||
|  | @ -53,8 +53,8 @@ module SystemNoteService | |||
|     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref) | ||||
|   end | ||||
| 
 | ||||
|   def unrelate_issue(noteable, noteable_ref, user) | ||||
|     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref) | ||||
|   def unrelate_issuable(noteable, noteable_ref, user) | ||||
|     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issuable(noteable_ref) | ||||
|   end | ||||
| 
 | ||||
|   # Called when the due_date of a Noteable is changed | ||||
|  |  | |||
|  | @ -26,8 +26,8 @@ module SystemNotes | |||
|     #   "removed the relation with gitlab-foss#9001" | ||||
|     # | ||||
|     # Returns the created Note object | ||||
|     def unrelate_issue(noteable_ref) | ||||
|       body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}" | ||||
|     def unrelate_issuable(noteable_ref) | ||||
|       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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -42,10 +42,20 @@ module WebHooks | |||
|           hook.failed! | ||||
|         end | ||||
|       end | ||||
|     rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError | ||||
|       raise if raise_lock_error? | ||||
|     end | ||||
| 
 | ||||
|     def lock_name | ||||
|       "web_hooks:update_hook_failure_state:#{hook.id}" | ||||
|     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 | ||||
|  |  | |||
|  | @ -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 } } | ||||
|   = 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) } | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ | |||
| - continuous_delivery | ||||
| - continuous_integration | ||||
| - continuous_integration_scaling | ||||
| - continuous_verification | ||||
| - database | ||||
| - dataops | ||||
| - delivery | ||||
|  | @ -74,7 +75,6 @@ | |||
| - kubernetes_management | ||||
| - license | ||||
| - license_compliance | ||||
| - live_preview | ||||
| - logging | ||||
| - memory | ||||
| - merge_trains | ||||
|  | @ -110,7 +110,6 @@ | |||
| - secrets_management | ||||
| - security_benchmarking | ||||
| - security_orchestration | ||||
| - self_monitoring | ||||
| - service_desk | ||||
| - service_ping | ||||
| - sharding | ||||
|  | @ -119,7 +118,6 @@ | |||
| - static_application_security_testing | ||||
| - static_site_editor | ||||
| - subgroups | ||||
| - synthetic_monitoring | ||||
| - team_planning | ||||
| - tracing | ||||
| - usage_ping | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| --- | ||||
| name: ci_pending_builds_maintain_denormalized_data | ||||
| 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' | ||||
| type: development | ||||
| group: group::pipeline execution | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| --- | ||||
| name: ci_pending_builds_queue_source | ||||
| 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' | ||||
| type: development | ||||
| group: group::pipeline execution | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| --- | ||||
| name: ci_queuing_use_denormalized_data_strategy | ||||
| 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' | ||||
| type: development | ||||
| group: group::pipeline execution | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| --- | ||||
| name: rearrange_pipelines_table | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72545 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343286 | ||||
| milestone: '14.8' | ||||
| name: container_registry_follow_redirects_middleware | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81056 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353291 | ||||
| milestone: '14.9' | ||||
| type: development | ||||
| group: group::pipeline execution | ||||
| default_enabled: true | ||||
| group: group::package | ||||
| default_enabled: false | ||||
|  | @ -62,3 +62,27 @@ | |||
|     - 'i_testing_group_code_coverage_visit_total' | ||||
|     - 'i_testing_load_performance_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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -2,7 +2,7 @@ | |||
|   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. | ||||
|   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 | ||||
|   reporter: jacobvosmaer-gitlab  # GitLab username of the person reporting the deprecation | ||||
|   body: |  # Do not modify this line, instead modify the lines below. | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|   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. | ||||
|   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 | ||||
|   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). | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|   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. | ||||
|   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 | ||||
|   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 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|   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. | ||||
|   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 | ||||
|   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). | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -0,0 +1 @@ | |||
| 688232dde01ea4e8574dca73459094264bde405d799ecaf1a5867adb72576b98 | ||||
|  | @ -0,0 +1 @@ | |||
| 84346c2f608792f259ab91dbc2c8aac8397a2997f890f8e077aad809276bb7cd | ||||
|  | @ -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_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_last_edited_by_id ON issues USING btree (last_edited_by_id); | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
| The number of pipelines that can be created in a single push is 4. | ||||
| This limit prevents the accidental creation of pipelines when `git push --all` | ||||
| or `git push --mirror` is used. | ||||
| When pushing multiple changes with a single Git push, like multiple tags or branches, | ||||
| only four tag or branch pipelines can be triggered. This limit prevents the accidental | ||||
| 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. | ||||
| All updated merge requests have a pipeline created when using | ||||
| [merge request pipelines](../ci/pipelines/merge_request_pipelines.md). | ||||
| [Merge request pipelines](../ci/pipelines/merge_request_pipelines.md) are not limited. | ||||
| If the Git push updates multiple merge requests at the same time, a merge request pipeline | ||||
| 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -10759,10 +10759,11 @@ Represents an epic board list. | |||
| | Name | Type | Description | | ||||
| | ---- | ---- | ----------- | | ||||
| | <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="epiclistlabel"></a>`label` | [`Label`](#label) | Label 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="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. | | ||||
| 
 | ||||
| ### `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` | ||||
| 
 | ||||
| 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="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. | | ||||
| 
 | ||||
| ### `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="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` | ||||
| 
 | ||||
| | Value | Description | | ||||
|  |  | |||
|  | @ -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 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 data refreshes daily. | ||||
| 
 | ||||
|  |  | |||
|  | @ -219,7 +219,7 @@ security issues: | |||
| WARNING: | ||||
| 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 | ||||
| [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 | ||||
| Vulnerability-Check rule. While this rule is enabled, additional merge request approval by | ||||
|  |  | |||
|  | @ -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. | ||||
| > - 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, | ||||
| you can refer to the cluster connection as a Kubernetes context. | ||||
| Then you can run Kubernetes API commands as part of your GitLab CI/CD pipeline. | ||||
| To do so, you must first [install an agent in your cluster](install/index.md). When done, you have a Kubernetes context and can | ||||
| run Kubernetes API commands in your GitLab CI/CD pipeline. | ||||
| 
 | ||||
| To ensure access to your cluster is safe: | ||||
| 
 | ||||
| - 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. | ||||
| 
 | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
| 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 | ||||
| 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. | ||||
| 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 | ||||
| 
 | ||||
| GitLab supports the following Kubernetes versions. You can upgrade your | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ module API | |||
|     desc 'Get the current application statistics' do | ||||
|       success Entities::ApplicationStatistics | ||||
|     end | ||||
|     get "application/statistics" do | ||||
|     get "application/statistics", urgency: :low do | ||||
|       counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS) | ||||
|       present counts, with: Entities::ApplicationStatistics | ||||
|     end | ||||
|  |  | |||
|  | @ -10,6 +10,21 @@ module ContainerRegistry | |||
|     REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features' | ||||
|     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? | ||||
|       with_dummy_client(return_value_if_disabled: false) do |client| | ||||
|         client.supports_tag_delete? | ||||
|  | @ -136,6 +151,10 @@ module ContainerRegistry | |||
|     def faraday_blob | ||||
|       @faraday_blob ||= faraday_base do |conn| | ||||
|         initialize_connection(conn, @options) | ||||
| 
 | ||||
|         if Feature.enabled?(:container_registry_follow_redirects_middleware) | ||||
|           conn.use ::FaradayMiddleware::FollowRedirects, REDIRECT_OPTIONS | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -16777,6 +16777,15 @@ msgstr "" | |||
| msgid "GithubIntegration|This requires mirroring your GitHub repository to this project. %{docs_link}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "GitlabTiers|Free" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "GitlabTiers|Premium" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "GitlabTiers|Ultimate" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Gitpod" | ||||
| 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." | ||||
| 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." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -27029,6 +27041,9 @@ msgstr "" | |||
| msgid "Pipelines|More Information" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Pipelines|Multi-project pipeline graphs" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Pipelines|No runners detected" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -27179,9 +27194,6 @@ msgstr "" | |||
| msgid "Pipeline|Checking pipeline status." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Pipeline|Commit" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -27197,9 +27209,6 @@ msgstr "" | |||
| msgid "Pipeline|Detached merge request pipeline" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Pipeline|Duration" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Pipeline|Failed" | ||||
| 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}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Preparing the report for the scan." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Prev" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -30798,6 +30810,12 @@ msgstr "" | |||
| msgid "Report abuse to admin" | ||||
| 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}" | ||||
| 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." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "The related CI build failed." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "The remote mirror URL is invalid." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -36840,6 +36861,12 @@ msgstr "" | |||
| msgid "The remote repository is being updated..." | ||||
| 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." | ||||
| 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)." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "The scan has been created." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "The snippet can be accessed without any authentication." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -37314,7 +37344,7 @@ msgstr "" | |||
| msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}." | ||||
| msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "This %{noteableTypeText} is locked." | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ module QA | |||
|           view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do | ||||
|             element :expand_pipeline_button | ||||
|             element :child_pipeline | ||||
|             element :linked_pipeline_body | ||||
|           end | ||||
| 
 | ||||
|           view 'app/assets/javascripts/reports/components/report_section.vue' do | ||||
|  | @ -93,7 +94,7 @@ module QA | |||
|           end | ||||
| 
 | ||||
|           def find_child_pipeline_by_title(title) | ||||
|             child_pipelines.find { |pipeline| pipeline[:title].include?(title) } | ||||
|             find_element(:child_pipeline, text: title) | ||||
|           end | ||||
| 
 | ||||
|           def expand_child_pipeline(title: nil) | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -56,7 +56,7 @@ module QA | |||
|             end | ||||
| 
 | ||||
|             Page::Project::Job::Show.perform do |show| | ||||
|               expect(show).to have_passed(timeout: 360) | ||||
|               expect(show).to have_passed(timeout: 800) | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|  |  | |||
|  | @ -125,7 +125,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do | |||
| 
 | ||||
|       before do | ||||
|         stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false) | ||||
|         stub_feature_flags(rearrange_pipelines_table: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a pipeline in the parent project when user proceeds with the warning' do | ||||
|  |  | |||
|  | @ -161,7 +161,7 @@ RSpec.describe 'Pipelines', :js do | |||
|         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 | ||||
|           create(:merge_request, | ||||
|                  :with_detached_merge_request_pipeline, | ||||
|  | @ -174,52 +174,6 @@ RSpec.describe 'Pipelines', :js do | |||
|         let(:target_project) { project } | ||||
| 
 | ||||
|         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) | ||||
|         end | ||||
| 
 | ||||
|  | @ -245,7 +199,7 @@ RSpec.describe 'Pipelines', :js do | |||
|         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 | ||||
|           create(:merge_request, | ||||
|                  :with_merge_request_pipeline, | ||||
|  | @ -259,53 +213,6 @@ RSpec.describe 'Pipelines', :js do | |||
|         let(:target_project) { project } | ||||
| 
 | ||||
|         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) | ||||
|         end | ||||
| 
 | ||||
|  | @ -676,28 +583,6 @@ RSpec.describe 'Pipelines', :js do | |||
| 
 | ||||
|       context 'with pipeline key selection' 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) | ||||
|           wait_for_requests | ||||
|         end | ||||
|  |  | |||
|  | @ -93,5 +93,38 @@ describe('DirtySubmitForm', () => { | |||
| 
 | ||||
|       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(); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui'; | ||||
| import { GlIntersectionObserver, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui'; | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import Vue from 'vue'; | ||||
| import VueApollo from 'vue-apollo'; | ||||
|  | @ -19,6 +19,7 @@ describe('Jobs app', () => { | |||
|   let resolverSpy; | ||||
| 
 | ||||
|   const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); | ||||
|   const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); | ||||
|   const findJobsTable = () => wrapper.findComponent(JobsTable); | ||||
| 
 | ||||
|   const triggerInfiniteScroll = () => | ||||
|  | @ -48,7 +49,29 @@ describe('Jobs app', () => { | |||
|     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); | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|     expect(findSkeletonLoader().exists()).toBe(true); | ||||
|  |  | |||
|  | @ -66,7 +66,6 @@ describe('graph component', () => { | |||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('with data', () => { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { GlButton, GlLoadingIcon } from '@gitlab/ui'; | ||||
| import { mount } from '@vue/test-utils'; | ||||
| import { GlButton, GlLoadingIcon, GlPopover } from '@gitlab/ui'; | ||||
| import { mountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; | ||||
| import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants'; | ||||
| import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; | ||||
|  | @ -9,15 +9,20 @@ import mockPipeline from './linked_pipelines_mock_data'; | |||
| describe('Linked pipeline', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const defaultProps = { | ||||
|     pipeline: mockPipeline, | ||||
|     columnTitle: 'Downstream', | ||||
|     type: DOWNSTREAM, | ||||
|     expanded: false, | ||||
|     isLoading: false, | ||||
|     isMultiProjectVizAvailable: true, | ||||
|   }; | ||||
| 
 | ||||
|   const downstreamProps = { | ||||
|     pipeline: { | ||||
|       ...mockPipeline, | ||||
|       multiproject: false, | ||||
|     }, | ||||
|     columnTitle: 'Downstream', | ||||
|     type: DOWNSTREAM, | ||||
|     expanded: false, | ||||
|     isLoading: false, | ||||
|   }; | ||||
| 
 | ||||
|   const upstreamProps = { | ||||
|  | @ -27,21 +32,29 @@ describe('Linked pipeline', () => { | |||
|   }; | ||||
| 
 | ||||
|   const findButton = () => wrapper.find(GlButton); | ||||
|   const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]'); | ||||
|   const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); | ||||
|   const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); | ||||
|   const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title'); | ||||
|   const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label'); | ||||
|   const findLinkedPipeline = () => wrapper.findByTestId('linkedPipeline'); | ||||
|   const findLinkedPipelineBody = () => wrapper.findByTestId('linkedPipelineBody'); | ||||
|   const findLoadingIcon = () => wrapper.find(GlLoadingIcon); | ||||
|   const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]'); | ||||
|   const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]'); | ||||
|   const findPipelineLink = () => wrapper.findByTestId('pipelineLink'); | ||||
|   const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button'); | ||||
|   const findPopover = () => wrapper.find(GlPopover); | ||||
| 
 | ||||
|   const createWrapper = (propsData, data = []) => { | ||||
|     wrapper = mount(LinkedPipelineComponent, { | ||||
|       propsData, | ||||
|     wrapper = mountExtended(LinkedPipelineComponent, { | ||||
|       propsData: { | ||||
|         ...defaultProps, | ||||
|         ...propsData, | ||||
|       }, | ||||
|       data() { | ||||
|         return { | ||||
|           ...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', () => { | ||||
|       const titleAttr = findLinkedPipeline().attributes('title'); | ||||
|       const titleAttr = findLinkedPipelineBody().attributes('title'); | ||||
| 
 | ||||
|       expect(titleAttr).toContain(mockPipeline.project.name); | ||||
|       expect(titleAttr).toContain(mockPipeline.status.label); | ||||
|  | @ -168,10 +181,6 @@ describe('Linked pipeline', () => { | |||
| 
 | ||||
|   describe('when isLoading is true', () => { | ||||
|     const props = { | ||||
|       pipeline: mockPipeline, | ||||
|       columnTitle: 'Downstream', | ||||
|       type: DOWNSTREAM, | ||||
|       expanded: false, | ||||
|       isLoading: true, | ||||
|     }; | ||||
| 
 | ||||
|  | @ -184,19 +193,43 @@ describe('Linked pipeline', () => { | |||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('on click/hover', () => { | ||||
|     const props = { | ||||
|       pipeline: mockPipeline, | ||||
|       columnTitle: 'Downstream', | ||||
|       type: DOWNSTREAM, | ||||
|       expanded: false, | ||||
|       isLoading: false, | ||||
|     }; | ||||
| 
 | ||||
|   describe('when the user does not have access to the multi-project pipeline viz feature', () => { | ||||
|     beforeEach(() => { | ||||
|       const props = { isMultiProjectVizAvailable: false }; | ||||
|       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', () => { | ||||
|       jest.spyOn(wrapper.vm, '$emit'); | ||||
|       findButton().trigger('click'); | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse); | |||
| describe('Linked Pipelines Column', () => { | ||||
|   const defaultProps = { | ||||
|     columnTitle: 'Downstream', | ||||
|     isMultiProjectVizAvailable: true, | ||||
|     linkedPipelines: processedPipeline.downstream, | ||||
|     showLinks: false, | ||||
|     type: DOWNSTREAM, | ||||
|  | @ -51,6 +52,9 @@ describe('Linked Pipelines Column', () => { | |||
|         ...defaultProps, | ||||
|         ...props, | ||||
|       }, | ||||
|       provide: { | ||||
|         multiProjectHelpPath: 'ci/pipelines/multi-project-pipeline', | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|  | @ -67,7 +71,6 @@ describe('Linked Pipelines Column', () => { | |||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('it renders correctly', () => { | ||||
|  |  | |||
|  | @ -13,6 +13,15 @@ export const mockPipelineResponse = { | |||
|         usesNeeds: true, | ||||
|         downstream: null, | ||||
|         upstream: null, | ||||
|         user: { | ||||
|           __typename: 'UserCore', | ||||
|           id: 'gid://gitlab/User/1', | ||||
|           namespace: { | ||||
|             __typename: 'Namespace', | ||||
|             id: 'gid://gitlab/Namespaces::UserNamespace/1', | ||||
|             crossProjectPipelineAvailable: true, | ||||
|           }, | ||||
|         }, | ||||
|         userPermissions: { | ||||
|           __typename: 'PipelinePermissions', | ||||
|           updatePipeline: true, | ||||
|  | @ -780,6 +789,15 @@ export const wrappedPipelineReturn = { | |||
|         id: 'gid://gitlab/Ci::Pipeline/175', | ||||
|         iid: '38', | ||||
|         complete: true, | ||||
|         user: { | ||||
|           __typename: 'UserCore', | ||||
|           id: 'gid://gitlab/User/1', | ||||
|           namespace: { | ||||
|             __typename: 'Namespace', | ||||
|             id: 'gid://gitlab/Namespaces::UserNamespace/1', | ||||
|             crossProjectPipelineAvailable: true, | ||||
|           }, | ||||
|         }, | ||||
|         usesNeeds: true, | ||||
|         userPermissions: { | ||||
|           __typename: 'PipelinePermissions', | ||||
|  |  | |||
|  | @ -30,14 +30,11 @@ describe('Pipeline Url Component', () => { | |||
| 
 | ||||
|   const defaultProps = mockPipeline(projectPath); | ||||
| 
 | ||||
|   const createComponent = (props, rearrangePipelinesTable = false) => { | ||||
|   const createComponent = (props) => { | ||||
|     wrapper = shallowMountExtended(PipelineUrlComponent, { | ||||
|       propsData: { ...defaultProps, ...props }, | ||||
|       provide: { | ||||
|         targetProjectFullPath: projectPath, | ||||
|         glFeatures: { | ||||
|           rearrangePipelinesTable, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
|  | @ -47,7 +44,6 @@ describe('Pipeline Url Component', () => { | |||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('with the rearrangePipelinesTable feature flag turned off', () => { | ||||
|   it('should render pipeline url table cell', () => { | ||||
|     createComponent(); | ||||
| 
 | ||||
|  | @ -194,15 +190,6 @@ describe('Pipeline Url Component', () => { | |||
|     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', () => { | ||||
|     createComponent({}, true); | ||||
| 
 | ||||
|  | @ -224,13 +211,9 @@ describe('Pipeline Url Component', () => { | |||
|     ${mockPipelineTag()}    | ${'Tag'} | ||||
|     ${mockPipelineBranch()} | ${'Branch'} | ||||
|     ${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); | ||||
| 
 | ||||
|     expect(findCommitIconType().attributes('title')).toBe(expectedTitle); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ import { | |||
| 
 | ||||
| import eventHub from '~/pipelines/event_hub'; | ||||
| import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; | ||||
| import CommitComponent from '~/vue_shared/components/commit.vue'; | ||||
| 
 | ||||
| jest.mock('~/pipelines/event_hub'); | ||||
| 
 | ||||
|  | @ -37,18 +36,13 @@ describe('Pipelines Table', () => { | |||
|     return pipelines.find((p) => p.user !== null && p.commit !== null); | ||||
|   }; | ||||
| 
 | ||||
|   const createComponent = (props = {}, rearrangePipelinesTable = false) => { | ||||
|   const createComponent = (props = {}) => { | ||||
|     wrapper = extendedWrapper( | ||||
|       mount(PipelinesTable, { | ||||
|         propsData: { | ||||
|           ...defaultProps, | ||||
|           ...props, | ||||
|         }, | ||||
|         provide: { | ||||
|           glFeatures: { | ||||
|             rearrangePipelinesTable, | ||||
|           }, | ||||
|         }, | ||||
|       }), | ||||
|     ); | ||||
|   }; | ||||
|  | @ -57,7 +51,6 @@ describe('Pipelines Table', () => { | |||
|   const findStatusBadge = () => wrapper.findComponent(CiBadge); | ||||
|   const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); | ||||
|   const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); | ||||
|   const findCommit = () => wrapper.findComponent(CommitComponent); | ||||
|   const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); | ||||
|   const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago); | ||||
|   const findActions = () => wrapper.findComponent(PipelineOperations); | ||||
|  | @ -65,10 +58,7 @@ describe('Pipelines Table', () => { | |||
|   const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); | ||||
|   const findStatusTh = () => wrapper.findByTestId('status-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 findTimeAgoTh = () => wrapper.findByTestId('timeago-th'); | ||||
|   const findActionsTh = () => wrapper.findByTestId('actions-th'); | ||||
|   const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); | ||||
|   const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); | ||||
|  | @ -82,7 +72,7 @@ describe('Pipelines Table', () => { | |||
|     wrapper = null; | ||||
|   }); | ||||
| 
 | ||||
|   describe('Pipelines Table with rearrangePipelinesTable feature flag turned off', () => { | ||||
|   describe('Pipelines Table', () => { | ||||
|     beforeEach(() => { | ||||
|       createComponent({ pipelines: [pipeline], viewType: 'root' }); | ||||
|     }); | ||||
|  | @ -93,11 +83,8 @@ describe('Pipelines Table', () => { | |||
| 
 | ||||
|     it('should render table head with correct columns', () => { | ||||
|       expect(findStatusTh().text()).toBe('Status'); | ||||
|       expect(findPipelineTh().text()).toBe('Pipeline ID'); | ||||
|       expect(findTriggererTh().text()).toBe('Triggerer'); | ||||
|       expect(findCommitTh().text()).toBe('Commit'); | ||||
|       expect(findPipelineTh().text()).toBe('Pipeline'); | ||||
|       expect(findStagesTh().text()).toBe('Stages'); | ||||
|       expect(findTimeAgoTh().text()).toBe('Duration'); | ||||
|       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', () => { | ||||
|       it('should render a pipeline mini graph', () => { | ||||
|         expect(findPipelineMiniGraph().exists()).toBe(true); | ||||
|  | @ -163,7 +129,7 @@ describe('Pipelines Table', () => { | |||
|           pipeline = createMockPipeline(); | ||||
|           pipeline.details.stages = null; | ||||
| 
 | ||||
|           createComponent({ pipelines: [pipeline] }, true); | ||||
|           createComponent({ pipelines: [pipeline] }); | ||||
|         }); | ||||
| 
 | ||||
|         it('stages are not rendered', () => { | ||||
|  | @ -176,7 +142,7 @@ describe('Pipelines Table', () => { | |||
|       }); | ||||
| 
 | ||||
|       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); | ||||
|       }); | ||||
|  | @ -207,30 +173,11 @@ describe('Pipelines Table', () => { | |||
|         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', () => { | ||||
|       it('should render the pipeline triggerer', () => { | ||||
|         expect(findTriggerer().exists()).toBe(true); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('commit cell', () => { | ||||
|       it('should not render commit information', () => { | ||||
|         expect(findCommit().exists()).toBe(false); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -34,21 +34,19 @@ exports[`Issue Warning Component when noteable is locked and confidential render | |||
| <span> | ||||
|   <span> | ||||
|     This issue is  | ||||
|     <a | ||||
|     <gl-link-stub | ||||
|       href="" | ||||
|       rel="noopener noreferrer" | ||||
|       target="_blank" | ||||
|     > | ||||
|       confidential | ||||
|     </a> | ||||
|     </gl-link-stub> | ||||
|      and  | ||||
|     <a | ||||
|     <gl-link-stub | ||||
|       href="" | ||||
|       rel="noopener noreferrer" | ||||
|       target="_blank" | ||||
|     > | ||||
|       locked | ||||
|     </a> | ||||
|     </gl-link-stub> | ||||
|     . | ||||
|   </span> | ||||
|    | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { GlIcon } from '@gitlab/ui'; | ||||
| import { GlIcon, GlSprintf } from '@gitlab/ui'; | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import { nextTick } from 'vue'; | ||||
| import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; | ||||
|  | @ -16,6 +16,9 @@ describe('Issue Warning Component', () => { | |||
|       propsData: { | ||||
|         ...props, | ||||
|       }, | ||||
|       stubs: { | ||||
|         GlSprintf, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|  |  | |||
|  | @ -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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -102,9 +102,9 @@ RSpec.describe Types::BaseEnum do | |||
|       it 'sets the values defined by the declarative enum' do | ||||
|         set_declarative_enum | ||||
| 
 | ||||
|         expect(enum_type.values.keys).to eq(['FOO']) | ||||
|         expect(enum_type.values.values.map(&:description)).to eq(['description of foo']) | ||||
|         expect(enum_type.values.values.map(&:value)).to eq([0]) | ||||
|         expect(enum_type.values.keys).to contain_exactly('FOO') | ||||
|         expect(enum_type.values.values.map(&:description)).to contain_exactly('description of foo') | ||||
|         expect(enum_type.values.values.map(&:value)).to contain_exactly('foo') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -168,24 +168,100 @@ RSpec.describe ContainerRegistry::Client do | |||
|       expect(subject).to eq('Blob') | ||||
|     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) | ||||
|           .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: | ||||
|         # 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| | ||||
|             !request.headers.include?('Authorization') | ||||
|           end | ||||
|           .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(subject).to eq('Successfully redirected') | ||||
|           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' | ||||
| 
 | ||||
|     # 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 | ||||
| 
 | ||||
|   describe '#upload_blob' do | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -4,40 +4,22 @@ require 'spec_helper' | |||
| 
 | ||||
| RSpec.describe IssueLinks::DestroyService do | ||||
|   describe '#execute' do | ||||
|     let(:project) { create(:project_empty_repo) } | ||||
|     let(:user) { create(:user) } | ||||
|     let_it_be(:project) { create(:project_empty_repo, :private) } | ||||
|     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 | ||||
|       let(:issue_a) { create(:issue, project: project) } | ||||
|       let(:issue_b) { create(:issue, project: project) } | ||||
|     subject { described_class.new(issuable_link, user).execute } | ||||
| 
 | ||||
|       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 | ||||
|         project.add_reporter(user) | ||||
|       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) } | ||||
| 
 | ||||
|       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 | ||||
| 
 | ||||
|     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 | ||||
|  |  | |||
|  | @ -117,7 +117,7 @@ RSpec.describe SystemNoteService do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.unrelate_issue' do | ||||
|   describe '.unrelate_issuable' do | ||||
|     let(:noteable_ref) { double } | ||||
|     let(:noteable) { double } | ||||
| 
 | ||||
|  | @ -127,10 +127,10 @@ RSpec.describe SystemNoteService do | |||
| 
 | ||||
|     it 'calls IssuableService' do | ||||
|       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 | ||||
| 
 | ||||
|       described_class.unrelate_issue(noteable, noteable_ref, double) | ||||
|       described_class.unrelate_issuable(noteable, noteable_ref, double) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,10 +30,10 @@ RSpec.describe ::SystemNotes::IssuablesService do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#unrelate_issue' do | ||||
|   describe '#unrelate_issuable' do | ||||
|     let(:noteable_ref) { create(:issue) } | ||||
| 
 | ||||
|     subject { service.unrelate_issue(noteable_ref) } | ||||
|     subject { service.unrelate_issuable(noteable_ref) } | ||||
| 
 | ||||
|     it_behaves_like 'a system note' do | ||||
|       let(:action) { 'unrelate' } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ require 'spec_helper' | |||
| 
 | ||||
| RSpec.describe WebHooks::LogExecutionService do | ||||
|   include ExclusiveLeaseHelpers | ||||
|   using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|   describe '#execute' do | ||||
|     around do |example| | ||||
|  | @ -34,11 +35,13 @@ RSpec.describe WebHooks::LogExecutionService do | |||
|       expect(WebHookLog.recent.first).to have_attributes(data) | ||||
|     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 | ||||
|         service = described_class.new(hook: project_hook, log_data: data, response_category: :error) | ||||
|         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) | ||||
| 
 | ||||
|         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) | ||||
|       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 | ||||
|       it 'does not increment the failure count' do | ||||
|         expect { service.execute }.not_to change(project_hook, :recent_failures) | ||||
|  |  | |||
|  | @ -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 | ||||
		Loading…
	
		Reference in New Issue