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 $ from 'jquery'; | ||||||
| import { memoize, throttle } from 'lodash'; | import { memoize, throttle } from 'lodash'; | ||||||
|  | import createEventHub from '~/helpers/event_hub_factory'; | ||||||
| 
 | 
 | ||||||
| class DirtySubmitForm { | class DirtySubmitForm { | ||||||
|   constructor(form) { |   constructor(form) { | ||||||
|     this.form = form; |     this.form = form; | ||||||
|     this.dirtyInputs = []; |     this.dirtyInputs = []; | ||||||
|     this.isDisabled = true; |     this.isDisabled = true; | ||||||
|  |     this.events = createEventHub(); | ||||||
| 
 | 
 | ||||||
|     this.init(); |     this.init(); | ||||||
|   } |   } | ||||||
|  | @ -36,11 +38,21 @@ class DirtySubmitForm { | ||||||
|     this.form.addEventListener('submit', (event) => this.formSubmit(event)); |     this.form.addEventListener('submit', (event) => this.formSubmit(event)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   addInputsListener(callback) { | ||||||
|  |     this.events.$on('input', callback); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   removeInputsListener(callback) { | ||||||
|  |     this.events.$off('input', callback); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   updateDirtyInput(event) { |   updateDirtyInput(event) { | ||||||
|     const { target } = event; |     const { target } = event; | ||||||
| 
 | 
 | ||||||
|     if (!target.dataset.isDirtySubmitInput) return; |     if (!target.dataset.isDirtySubmitInput) return; | ||||||
| 
 | 
 | ||||||
|  |     this.events.$emit('input', event); | ||||||
|  | 
 | ||||||
|     this.updateDirtyInputs(target); |     this.updateDirtyInputs(target); | ||||||
|     this.toggleSubmission(); |     this.toggleSubmission(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -92,6 +92,9 @@ export default { | ||||||
|     hasUpstreamPipelines() { |     hasUpstreamPipelines() { | ||||||
|       return Boolean(this.pipeline?.upstream?.length > 0); |       return Boolean(this.pipeline?.upstream?.length > 0); | ||||||
|     }, |     }, | ||||||
|  |     isMultiProjectVizAvailable() { | ||||||
|  |       return Boolean(this.pipeline?.user?.namespace?.crossProjectPipelineAvailable); | ||||||
|  |     }, | ||||||
|     isStageView() { |     isStageView() { | ||||||
|       return this.viewType === STAGE_VIEW; |       return this.viewType === STAGE_VIEW; | ||||||
|     }, |     }, | ||||||
|  | @ -178,6 +181,7 @@ export default { | ||||||
|           <linked-pipelines-column |           <linked-pipelines-column | ||||||
|             v-if="showUpstreamPipelines" |             v-if="showUpstreamPipelines" | ||||||
|             :config-paths="configPaths" |             :config-paths="configPaths" | ||||||
|  |             :is-multi-project-viz-available="isMultiProjectVizAvailable" | ||||||
|             :linked-pipelines="upstreamPipelines" |             :linked-pipelines="upstreamPipelines" | ||||||
|             :column-title="__('Upstream')" |             :column-title="__('Upstream')" | ||||||
|             :show-links="showJobLinks" |             :show-links="showJobLinks" | ||||||
|  | @ -226,6 +230,7 @@ export default { | ||||||
|             v-if="showDownstreamPipelines" |             v-if="showDownstreamPipelines" | ||||||
|             class="gl-mr-6" |             class="gl-mr-6" | ||||||
|             :config-paths="configPaths" |             :config-paths="configPaths" | ||||||
|  |             :is-multi-project-viz-available="isMultiProjectVizAvailable" | ||||||
|             :linked-pipelines="downstreamPipelines" |             :linked-pipelines="downstreamPipelines" | ||||||
|             :column-title="__('Downstream')" |             :column-title="__('Downstream')" | ||||||
|             :show-links="showJobLinks" |             :show-links="showJobLinks" | ||||||
|  |  | ||||||
|  | @ -1,12 +1,29 @@ | ||||||
| <script> | <script> | ||||||
| import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; | import { | ||||||
|  |   GlBadge, | ||||||
|  |   GlButton, | ||||||
|  |   GlLink, | ||||||
|  |   GlLoadingIcon, | ||||||
|  |   GlPopover, | ||||||
|  |   GlSprintf, | ||||||
|  |   GlTooltipDirective, | ||||||
|  | } from '@gitlab/ui'; | ||||||
|  | import TierBadge from '~/vue_shared/components/tier_badge.vue'; | ||||||
| import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; | import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; | ||||||
| import { __, sprintf } from '~/locale'; | import { __, s__, sprintf } from '~/locale'; | ||||||
| import CiStatus from '~/vue_shared/components/ci_icon.vue'; | import CiStatus from '~/vue_shared/components/ci_icon.vue'; | ||||||
| import { reportToSentry } from '../../utils'; | import { reportToSentry } from '../../utils'; | ||||||
| import { DOWNSTREAM, UPSTREAM } from './constants'; | import { DOWNSTREAM, UPSTREAM } from './constants'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  |   i18n: { | ||||||
|  |     popover: { | ||||||
|  |       title: s__('Pipelines|Multi-project pipeline graphs'), | ||||||
|  |       description: s__( | ||||||
|  |         'Pipelines|Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines. %{linkStart}Learn More%{linkEnd}', | ||||||
|  |       ), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|   directives: { |   directives: { | ||||||
|     GlTooltip: GlTooltipDirective, |     GlTooltip: GlTooltipDirective, | ||||||
|   }, |   }, | ||||||
|  | @ -16,7 +33,11 @@ export default { | ||||||
|     GlButton, |     GlButton, | ||||||
|     GlLink, |     GlLink, | ||||||
|     GlLoadingIcon, |     GlLoadingIcon, | ||||||
|  |     GlPopover, | ||||||
|  |     GlSprintf, | ||||||
|  |     TierBadge, | ||||||
|   }, |   }, | ||||||
|  |   inject: ['multiProjectHelpPath'], | ||||||
|   props: { |   props: { | ||||||
|     columnTitle: { |     columnTitle: { | ||||||
|       type: String, |       type: String, | ||||||
|  | @ -26,6 +47,10 @@ export default { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|  |     isMultiProjectVizAvailable: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|     isLoading: { |     isLoading: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       required: true, |       required: true, | ||||||
|  | @ -90,6 +115,9 @@ export default { | ||||||
|     pipelineStatus() { |     pipelineStatus() { | ||||||
|       return this.pipeline.status; |       return this.pipeline.status; | ||||||
|     }, |     }, | ||||||
|  |     popoverContainerId() { | ||||||
|  |       return `popoverContainer-${this.pipeline.id}`; | ||||||
|  |     }, | ||||||
|     projectName() { |     projectName() { | ||||||
|       return this.pipeline.project.name; |       return this.pipeline.project.name; | ||||||
|     }, |     }, | ||||||
|  | @ -128,16 +156,21 @@ export default { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     ref="linkedPipeline" |  | ||||||
|     v-gl-tooltip |  | ||||||
|     class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1" |     class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1" | ||||||
|     :class="flexDirection" |     :class="flexDirection" | ||||||
|     :title="tooltipText" |  | ||||||
|     data-qa-selector="child_pipeline" |     data-qa-selector="child_pipeline" | ||||||
|  |     data-testid="linkedPipeline" | ||||||
|     @mouseover="onDownstreamHovered" |     @mouseover="onDownstreamHovered" | ||||||
|     @mouseleave="onDownstreamHoverLeave" |     @mouseleave="onDownstreamHoverLeave" | ||||||
|   > |   > | ||||||
|     <div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass"> |     <div | ||||||
|  |       v-gl-tooltip | ||||||
|  |       class="gl-w-full gl-bg-white gl-p-3" | ||||||
|  |       :class="cardSpacingClass" | ||||||
|  |       data-testid="linkedPipelineBody" | ||||||
|  |       data-qa-selector="linked_pipeline_body" | ||||||
|  |       :title="tooltipText" | ||||||
|  |     > | ||||||
|       <div class="gl-display-flex gl-pr-3"> |       <div class="gl-display-flex gl-pr-3"> | ||||||
|         <ci-status |         <ci-status | ||||||
|           v-if="!pipelineIsLoading" |           v-if="!pipelineIsLoading" | ||||||
|  | @ -163,17 +196,38 @@ export default { | ||||||
|         </gl-badge> |         </gl-badge> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="gl-display-flex"> |     <div :id="popoverContainerId" class="gl-display-flex"> | ||||||
|       <gl-button |       <gl-button | ||||||
|         :id="buttonId" |         :id="buttonId" | ||||||
|         class="gl-shadow-none! gl-rounded-0!" |         class="gl-shadow-none! gl-rounded-0!" | ||||||
|         :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`" |         :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`" | ||||||
|         :icon="expandedIcon" |         :icon="expandedIcon" | ||||||
|         :aria-label="__('Expand pipeline')" |         :aria-label="__('Expand pipeline')" | ||||||
|  |         :disabled="!isMultiProjectVizAvailable" | ||||||
|         data-testid="expand-pipeline-button" |         data-testid="expand-pipeline-button" | ||||||
|         data-qa-selector="expand_pipeline_button" |         data-qa-selector="expand_pipeline_button" | ||||||
|         @click="onClickLinkedPipeline" |         @click="onClickLinkedPipeline" | ||||||
|       /> |       /> | ||||||
|  |       <gl-popover | ||||||
|  |         v-if="!isMultiProjectVizAvailable" | ||||||
|  |         placement="top" | ||||||
|  |         :target="popoverContainerId" | ||||||
|  |         triggers="hover" | ||||||
|  |       > | ||||||
|  |         <template #title> | ||||||
|  |           <b>{{ $options.i18n.popover.title }}</b> | ||||||
|  |           <tier-badge class="gl-mt-3" tier="premium" | ||||||
|  |         /></template> | ||||||
|  |         <p class="gl-my-0"> | ||||||
|  |           <gl-sprintf :message="$options.i18n.popover.description"> | ||||||
|  |             <template #link="{ content }" | ||||||
|  |               ><gl-link :href="multiProjectHelpPath" class="gl-font-sm" target="_blank">{{ | ||||||
|  |                 content | ||||||
|  |               }}</gl-link> | ||||||
|  |             </template> | ||||||
|  |           </gl-sprintf> | ||||||
|  |         </p> | ||||||
|  |       </gl-popover> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -28,6 +28,10 @@ export default { | ||||||
|       required: true, |       required: true, | ||||||
|       validator: validateConfigPaths, |       validator: validateConfigPaths, | ||||||
|     }, |     }, | ||||||
|  |     isMultiProjectVizAvailable: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|     linkedPipelines: { |     linkedPipelines: { | ||||||
|       type: Array, |       type: Array, | ||||||
|       required: true, |       required: true, | ||||||
|  | @ -208,6 +212,7 @@ export default { | ||||||
|           <linked-pipeline |           <linked-pipeline | ||||||
|             class="gl-display-inline-block" |             class="gl-display-inline-block" | ||||||
|             :is-loading="isLoadingPipeline(pipeline.id)" |             :is-loading="isLoadingPipeline(pipeline.id)" | ||||||
|  |             :is-multi-project-viz-available="isMultiProjectVizAvailable" | ||||||
|             :pipeline="pipeline" |             :pipeline="pipeline" | ||||||
|             :column-title="columnTitle" |             :column-title="columnTitle" | ||||||
|             :type="type" |             :type="type" | ||||||
|  |  | ||||||
|  | @ -60,6 +60,15 @@ export default { | ||||||
|         iid: this.pipelineIid, |         iid: this.pipelineIid, | ||||||
|       }; |       }; | ||||||
|     }, |     }, | ||||||
|  |     loading() { | ||||||
|  |       return this.$apollo.queries.jobs.loading; | ||||||
|  |     }, | ||||||
|  |     showSkeletonLoader() { | ||||||
|  |       return this.firstLoad && this.loading; | ||||||
|  |     }, | ||||||
|  |     showLoadingSpinner() { | ||||||
|  |       return !this.firstLoad && this.loading; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     eventHub.$on('jobActionPerformed', this.handleJobAction); |     eventHub.$on('jobActionPerformed', this.handleJobAction); | ||||||
|  | @ -69,7 +78,7 @@ export default { | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     handleJobAction() { |     handleJobAction() { | ||||||
|       this.firstLoad = true; |       this.firstLoad = false; | ||||||
| 
 | 
 | ||||||
|       this.$apollo.queries.jobs.refetch(); |       this.$apollo.queries.jobs.refetch(); | ||||||
|     }, |     }, | ||||||
|  | @ -98,7 +107,7 @@ export default { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div v-if="$apollo.loading && firstLoad" class="gl-mt-5"> |     <div v-if="showSkeletonLoader" class="gl-mt-5"> | ||||||
|       <gl-skeleton-loader :width="1248" :height="73"> |       <gl-skeleton-loader :width="1248" :height="73"> | ||||||
|         <circle cx="748.031" cy="37.7193" r="15.0307" /> |         <circle cx="748.031" cy="37.7193" r="15.0307" /> | ||||||
|         <circle cx="787.241" cy="37.7193" r="15.0307" /> |         <circle cx="787.241" cy="37.7193" r="15.0307" /> | ||||||
|  | @ -118,7 +127,7 @@ export default { | ||||||
|     <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" /> |     <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" /> | ||||||
| 
 | 
 | ||||||
|     <gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs"> |     <gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs"> | ||||||
|       <gl-loading-icon v-if="$apollo.loading" size="md" /> |       <gl-loading-icon v-if="showLoadingSpinner" size="md" /> | ||||||
|     </gl-intersection-observer> |     </gl-intersection-observer> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| <script> | <script> | ||||||
| import { GlIcon, GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; | import { GlIcon, GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; | ||||||
| import { __, sprintf } from '~/locale'; | import { __ } from '~/locale'; | ||||||
| import { helpPagePath } from '~/helpers/help_page_helper'; | import { helpPagePath } from '~/helpers/help_page_helper'; | ||||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; |  | ||||||
| import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; | import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; | ||||||
| import { SCHEDULE_ORIGIN, ICONS } from '../../constants'; | import { SCHEDULE_ORIGIN, ICONS } from '../../constants'; | ||||||
| 
 | 
 | ||||||
|  | @ -18,7 +17,6 @@ export default { | ||||||
|   directives: { |   directives: { | ||||||
|     GlTooltip: GlTooltipDirective, |     GlTooltip: GlTooltipDirective, | ||||||
|   }, |   }, | ||||||
|   mixins: [glFeatureFlagMixin()], |  | ||||||
|   inject: { |   inject: { | ||||||
|     targetProjectFullPath: { |     targetProjectFullPath: { | ||||||
|       default: '', |       default: '', | ||||||
|  | @ -139,28 +137,14 @@ export default { | ||||||
|     commitTitle() { |     commitTitle() { | ||||||
|       return this.pipeline?.commit?.title; |       return this.pipeline?.commit?.title; | ||||||
|     }, |     }, | ||||||
|     hasAuthor() { |  | ||||||
|       return ( |  | ||||||
|         this.commitAuthor?.avatar_url && this.commitAuthor?.path && this.commitAuthor?.username |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     userImageAltDescription() { |  | ||||||
|       return this.commitAuthor?.username |  | ||||||
|         ? sprintf(__("%{username}'s avatar"), { username: this.commitAuthor.username }) |  | ||||||
|         : null; |  | ||||||
|     }, |  | ||||||
|     rearrangePipelinesTable() { |  | ||||||
|       return this.glFeatures?.rearrangePipelinesTable; |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div class="pipeline-tags" data-testid="pipeline-url-table-cell"> |   <div class="pipeline-tags" data-testid="pipeline-url-table-cell"> | ||||||
|     <template v-if="rearrangePipelinesTable"> |  | ||||||
|     <div class="commit-title gl-mb-2" data-testid="commit-title-container"> |     <div class="commit-title gl-mb-2" data-testid="commit-title-container"> | ||||||
|       <span v-if="commitTitle" class="gl-display-flex"> |       <span v-if="commitTitle" class="gl-display-flex"> | ||||||
|           <tooltip-on-truncate :title="commitTitle" class="flex-truncate-child gl-flex-grow-1"> |         <tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate"> | ||||||
|           <gl-link |           <gl-link | ||||||
|             :href="commitUrl" |             :href="commitUrl" | ||||||
|             class="commit-row-message gl-text-gray-900" |             class="commit-row-message gl-text-gray-900" | ||||||
|  | @ -214,16 +198,6 @@ export default { | ||||||
|       }}</gl-link> |       }}</gl-link> | ||||||
|       <!--End of commit row--> |       <!--End of commit row--> | ||||||
|     </div> |     </div> | ||||||
|     </template> |  | ||||||
|     <gl-link |  | ||||||
|       v-if="!rearrangePipelinesTable" |  | ||||||
|       :href="pipeline.path" |  | ||||||
|       class="gl-text-decoration-underline" |  | ||||||
|       data-testid="pipeline-url-link" |  | ||||||
|       data-qa-selector="pipeline_url_link" |  | ||||||
|     > |  | ||||||
|       #{{ pipeline[pipelineKey] }} |  | ||||||
|     </gl-link> |  | ||||||
|     <div class="label-container gl-mt-1"> |     <div class="label-container gl-mt-1"> | ||||||
|       <gl-badge |       <gl-badge | ||||||
|         v-if="isScheduled" |         v-if="isScheduled" | ||||||
|  |  | ||||||
|  | @ -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 { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants'; | ||||||
| import { CHILD_VIEW } from '~/pipelines/constants'; | import { CHILD_VIEW } from '~/pipelines/constants'; | ||||||
| import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; | import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; | ||||||
| import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; |  | ||||||
| import PipelinesTimeago from './time_ago.vue'; | import PipelinesTimeago from './time_ago.vue'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  | @ -12,7 +11,6 @@ export default { | ||||||
|     CiBadge, |     CiBadge, | ||||||
|     PipelinesTimeago, |     PipelinesTimeago, | ||||||
|   }, |   }, | ||||||
|   mixins: [glFeatureFlagsMixin()], |  | ||||||
|   props: { |   props: { | ||||||
|     pipeline: { |     pipeline: { | ||||||
|       type: Object, |       type: Object, | ||||||
|  | @ -44,9 +42,6 @@ export default { | ||||||
|     codeQualityBuildPath() { |     codeQualityBuildPath() { | ||||||
|       return this.pipeline?.details?.code_quality_build_path; |       return this.pipeline?.details?.code_quality_build_path; | ||||||
|     }, |     }, | ||||||
|     rearrangePipelinesTable() { |  | ||||||
|       return this.glFeatures?.rearrangePipelinesTable; |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  | @ -61,7 +56,7 @@ export default { | ||||||
|       :icon-classes="'gl-vertical-align-middle!'" |       :icon-classes="'gl-vertical-align-middle!'" | ||||||
|       data-qa-selector="pipeline_commit_status" |       data-qa-selector="pipeline_commit_status" | ||||||
|     /> |     /> | ||||||
|     <pipelines-timeago v-if="rearrangePipelinesTable" class="gl-mt-3" :pipeline="pipeline" /> |     <pipelines-timeago class="gl-mt-3" :pipeline="pipeline" /> | ||||||
|     <code-quality-walkthrough |     <code-quality-walkthrough | ||||||
|       v-if="shouldRenderCodeQualityWalkthrough" |       v-if="shouldRenderCodeQualityWalkthrough" | ||||||
|       :step="codeQualityStep" |       :step="codeQualityStep" | ||||||
|  |  | ||||||
|  | @ -1,16 +1,13 @@ | ||||||
| <script> | <script> | ||||||
| import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; | import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; | ||||||
| import { s__, __ } from '~/locale'; | import { s__, __ } from '~/locale'; | ||||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; |  | ||||||
| import eventHub from '../../event_hub'; | import eventHub from '../../event_hub'; | ||||||
| import PipelineMiniGraph from './pipeline_mini_graph.vue'; | import PipelineMiniGraph from './pipeline_mini_graph.vue'; | ||||||
| import PipelineOperations from './pipeline_operations.vue'; | import PipelineOperations from './pipeline_operations.vue'; | ||||||
| import PipelineStopModal from './pipeline_stop_modal.vue'; | import PipelineStopModal from './pipeline_stop_modal.vue'; | ||||||
| import PipelineTriggerer from './pipeline_triggerer.vue'; | import PipelineTriggerer from './pipeline_triggerer.vue'; | ||||||
| import PipelineUrl from './pipeline_url.vue'; | import PipelineUrl from './pipeline_url.vue'; | ||||||
| import PipelinesCommit from './pipelines_commit.vue'; |  | ||||||
| import PipelinesStatusBadge from './pipelines_status_badge.vue'; | import PipelinesStatusBadge from './pipelines_status_badge.vue'; | ||||||
| import PipelinesTimeago from './time_ago.vue'; |  | ||||||
| 
 | 
 | ||||||
| const DEFAULT_TD_CLASS = 'gl-p-5!'; | const DEFAULT_TD_CLASS = 'gl-p-5!'; | ||||||
| const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; | const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; | ||||||
|  | @ -22,19 +19,16 @@ export default { | ||||||
|     GlTableLite, |     GlTableLite, | ||||||
|     LinkedPipelinesMiniList: () => |     LinkedPipelinesMiniList: () => | ||||||
|       import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), |       import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), | ||||||
|     PipelinesCommit, |  | ||||||
|     PipelineMiniGraph, |     PipelineMiniGraph, | ||||||
|     PipelineOperations, |     PipelineOperations, | ||||||
|     PipelinesStatusBadge, |     PipelinesStatusBadge, | ||||||
|     PipelineStopModal, |     PipelineStopModal, | ||||||
|     PipelinesTimeago, |  | ||||||
|     PipelineTriggerer, |     PipelineTriggerer, | ||||||
|     PipelineUrl, |     PipelineUrl, | ||||||
|   }, |   }, | ||||||
|   directives: { |   directives: { | ||||||
|     GlTooltip: GlTooltipDirective, |     GlTooltip: GlTooltipDirective, | ||||||
|   }, |   }, | ||||||
|   mixins: [glFeatureFlagMixin()], |  | ||||||
|   props: { |   props: { | ||||||
|     pipelines: { |     pipelines: { | ||||||
|       type: Array, |       type: Array, | ||||||
|  | @ -74,18 +68,16 @@ export default { | ||||||
|           key: 'status', |           key: 'status', | ||||||
|           label: s__('Pipeline|Status'), |           label: s__('Pipeline|Status'), | ||||||
|           thClass: DEFAULT_TH_CLASSES, |           thClass: DEFAULT_TH_CLASSES, | ||||||
|           columnClass: this.rearrangePipelinesTable ? 'gl-w-15p' : 'gl-w-10p', |           columnClass: 'gl-w-15p', | ||||||
|           tdClass: DEFAULT_TD_CLASS, |           tdClass: DEFAULT_TD_CLASS, | ||||||
|           thAttr: { 'data-testid': 'status-th' }, |           thAttr: { 'data-testid': 'status-th' }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           key: 'pipeline', |           key: 'pipeline', | ||||||
|           label: this.rearrangePipelinesTable ? __('Pipeline') : this.pipelineKeyOption.label, |           label: __('Pipeline'), | ||||||
|           thClass: DEFAULT_TH_CLASSES, |           thClass: DEFAULT_TH_CLASSES, | ||||||
|           tdClass: this.rearrangePipelinesTable |           tdClass: `${DEFAULT_TD_CLASS}`, | ||||||
|             ? `${DEFAULT_TD_CLASS}` |           columnClass: 'gl-w-30p', | ||||||
|             : `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, |  | ||||||
|           columnClass: this.rearrangePipelinesTable ? 'gl-w-30p' : 'gl-w-10p', |  | ||||||
|           thAttr: { 'data-testid': 'pipeline-th' }, |           thAttr: { 'data-testid': 'pipeline-th' }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|  | @ -96,14 +88,6 @@ export default { | ||||||
|           columnClass: 'gl-w-10p', |           columnClass: 'gl-w-10p', | ||||||
|           thAttr: { 'data-testid': 'triggerer-th' }, |           thAttr: { 'data-testid': 'triggerer-th' }, | ||||||
|         }, |         }, | ||||||
|         { |  | ||||||
|           key: 'commit', |  | ||||||
|           label: s__('Pipeline|Commit'), |  | ||||||
|           thClass: DEFAULT_TH_CLASSES, |  | ||||||
|           tdClass: DEFAULT_TD_CLASS, |  | ||||||
|           columnClass: 'gl-w-20p', |  | ||||||
|           thAttr: { 'data-testid': 'commit-th' }, |  | ||||||
|         }, |  | ||||||
|         { |         { | ||||||
|           key: 'stages', |           key: 'stages', | ||||||
|           label: s__('Pipeline|Stages'), |           label: s__('Pipeline|Stages'), | ||||||
|  | @ -112,14 +96,6 @@ export default { | ||||||
|           columnClass: 'gl-w-quarter', |           columnClass: 'gl-w-quarter', | ||||||
|           thAttr: { 'data-testid': 'stages-th' }, |           thAttr: { 'data-testid': 'stages-th' }, | ||||||
|         }, |         }, | ||||||
|         { |  | ||||||
|           key: 'timeago', |  | ||||||
|           label: s__('Pipeline|Duration'), |  | ||||||
|           thClass: DEFAULT_TH_CLASSES, |  | ||||||
|           tdClass: DEFAULT_TD_CLASS, |  | ||||||
|           columnClass: this.rearrangePipelinesTable ? 'gl-w-5p' : 'gl-w-15p', |  | ||||||
|           thAttr: { 'data-testid': 'timeago-th' }, |  | ||||||
|         }, |  | ||||||
|         { |         { | ||||||
|           key: 'actions', |           key: 'actions', | ||||||
|           thClass: DEFAULT_TH_CLASSES, |           thClass: DEFAULT_TH_CLASSES, | ||||||
|  | @ -129,12 +105,7 @@ export default { | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
| 
 | 
 | ||||||
|       return !this.rearrangePipelinesTable |       return fields; | ||||||
|         ? fields |  | ||||||
|         : fields.filter((field) => !['commit', 'timeago'].includes(field.key)); |  | ||||||
|     }, |  | ||||||
|     rearrangePipelinesTable() { |  | ||||||
|       return this.glFeatures?.rearrangePipelinesTable; |  | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|  | @ -200,10 +171,6 @@ export default { | ||||||
|         <pipeline-triggerer :pipeline="item" /> |         <pipeline-triggerer :pipeline="item" /> | ||||||
|       </template> |       </template> | ||||||
| 
 | 
 | ||||||
|       <template #cell(commit)="{ item }"> |  | ||||||
|         <pipelines-commit :pipeline="item" :view-type="viewType" /> |  | ||||||
|       </template> |  | ||||||
| 
 |  | ||||||
|       <template #cell(stages)="{ item }"> |       <template #cell(stages)="{ item }"> | ||||||
|         <div class="stage-cell"> |         <div class="stage-cell"> | ||||||
|           <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> |           <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> | ||||||
|  | @ -229,10 +196,6 @@ export default { | ||||||
|         </div> |         </div> | ||||||
|       </template> |       </template> | ||||||
| 
 | 
 | ||||||
|       <template #cell(timeago)="{ item }"> |  | ||||||
|         <pipelines-timeago :pipeline="item" /> |  | ||||||
|       </template> |  | ||||||
| 
 |  | ||||||
|       <template #cell(actions)="{ item }"> |       <template #cell(actions)="{ item }"> | ||||||
|         <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> |         <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> | ||||||
|       </template> |       </template> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| <script> | <script> | ||||||
| import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; | import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; | ||||||
| import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; |  | ||||||
| import timeagoMixin from '~/vue_shared/mixins/timeago'; | import timeagoMixin from '~/vue_shared/mixins/timeago'; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  | @ -8,7 +7,7 @@ export default { | ||||||
|     GlTooltip: GlTooltipDirective, |     GlTooltip: GlTooltipDirective, | ||||||
|   }, |   }, | ||||||
|   components: { GlIcon }, |   components: { GlIcon }, | ||||||
|   mixins: [timeagoMixin, glFeatureFlagMixin()], |   mixins: [timeagoMixin], | ||||||
|   props: { |   props: { | ||||||
|     pipeline: { |     pipeline: { | ||||||
|       type: Object, |       type: Object, | ||||||
|  | @ -54,14 +53,11 @@ export default { | ||||||
|     showSkipped() { |     showSkipped() { | ||||||
|       return !this.duration && !this.finishedTime && this.skipped; |       return !this.duration && !this.finishedTime && this.skipped; | ||||||
|     }, |     }, | ||||||
|     shouldDisplayAsBlock() { |  | ||||||
|       return this.glFeatures?.rearrangePipelinesTable; |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <template> | <template> | ||||||
|   <div class="{ 'gl-display-block': shouldDisplayAsBlock }"> |   <div class="gl-display-block"> | ||||||
|     <span v-if="showInProgress" data-testid="pipeline-in-progress"> |     <span v-if="showInProgress" data-testid="pipeline-in-progress"> | ||||||
|       <gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" /> |       <gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" /> | ||||||
|       <gl-icon |       <gl-icon | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ Vue.use(VueApollo); | ||||||
| const createPipelinesDetailApp = ( | const createPipelinesDetailApp = ( | ||||||
|   selector, |   selector, | ||||||
|   apolloProvider, |   apolloProvider, | ||||||
|   { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {}, |   { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag, multiProjectHelpPath } = {}, | ||||||
| ) => { | ) => { | ||||||
|   // eslint-disable-next-line no-new
 |   // eslint-disable-next-line no-new
 | ||||||
|   new Vue({ |   new Vue({ | ||||||
|  | @ -22,6 +22,7 @@ const createPipelinesDetailApp = ( | ||||||
|       pipelineProjectPath, |       pipelineProjectPath, | ||||||
|       pipelineIid, |       pipelineIid, | ||||||
|       graphqlResourceEtag, |       graphqlResourceEtag, | ||||||
|  |       multiProjectHelpPath, | ||||||
|     }, |     }, | ||||||
|     errorCaptured(err, _vm, info) { |     errorCaptured(err, _vm, info) { | ||||||
|       reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`); |       reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`); | ||||||
|  |  | ||||||
|  | @ -1,12 +1,7 @@ | ||||||
| <script> | <script> | ||||||
| import { GlLink, GlIcon } from '@gitlab/ui'; | import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui'; | ||||||
| import { escape } from 'lodash'; |  | ||||||
| import { __, sprintf } from '~/locale'; | import { __, sprintf } from '~/locale'; | ||||||
| 
 | 
 | ||||||
| function buildDocsLinkStart(path) { |  | ||||||
|   return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const NoteableTypeText = { | const NoteableTypeText = { | ||||||
|   Issue: __('issue'), |   Issue: __('issue'), | ||||||
|   Epic: __('epic'), |   Epic: __('epic'), | ||||||
|  | @ -17,6 +12,7 @@ export default { | ||||||
|   components: { |   components: { | ||||||
|     GlIcon, |     GlIcon, | ||||||
|     GlLink, |     GlLink, | ||||||
|  |     GlSprintf, | ||||||
|   }, |   }, | ||||||
|   props: { |   props: { | ||||||
|     isLocked: { |     isLocked: { | ||||||
|  | @ -59,20 +55,6 @@ export default { | ||||||
|     noteableTypeText() { |     noteableTypeText() { | ||||||
|       return NoteableTypeText[this.noteableType]; |       return NoteableTypeText[this.noteableType]; | ||||||
|     }, |     }, | ||||||
|     confidentialAndLockedDiscussionText() { |  | ||||||
|       return sprintf( |  | ||||||
|         __( |  | ||||||
|           'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.', |  | ||||||
|         ), |  | ||||||
|         { |  | ||||||
|           noteableTypeText: this.noteableTypeText, |  | ||||||
|           confidentialLinkStart: buildDocsLinkStart(this.confidentialNoteableDocsPath), |  | ||||||
|           lockedLinkStart: buildDocsLinkStart(this.lockedNoteableDocsPath), |  | ||||||
|           linkEnd: '</a>', |  | ||||||
|         }, |  | ||||||
|         false, |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     confidentialContextText() { |     confidentialContextText() { | ||||||
|       return sprintf(__('This is a confidential %{noteableTypeText}.'), { |       return sprintf(__('This is a confidential %{noteableTypeText}.'), { | ||||||
|         noteableTypeText: this.noteableTypeText, |         noteableTypeText: this.noteableTypeText, | ||||||
|  | @ -91,9 +73,23 @@ export default { | ||||||
|     <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> |     <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> | ||||||
| 
 | 
 | ||||||
|     <span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> |     <span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> | ||||||
|       <span |       <span> | ||||||
|         v-html="confidentialAndLockedDiscussionText /* eslint-disable-line vue/no-v-html */" |         <gl-sprintf | ||||||
|       ></span> |           :message=" | ||||||
|  |             __( | ||||||
|  |               'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}.', | ||||||
|  |             ) | ||||||
|  |           " | ||||||
|  |         > | ||||||
|  |           <template #noteableTypeText>{{ noteableTypeText }}</template> | ||||||
|  |           <template #confidentialLink="{ content }"> | ||||||
|  |             <gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ content }}</gl-link> | ||||||
|  |           </template> | ||||||
|  |           <template #lockedLink="{ content }"> | ||||||
|  |             <gl-link :href="lockedNoteableDocsPath" target="_blank">{{ content }}</gl-link> | ||||||
|  |           </template> | ||||||
|  |         </gl-sprintf> | ||||||
|  |       </span> | ||||||
|       {{ |       {{ | ||||||
|         __("People without permission will never get a notification and won't be able to comment.") |         __("People without permission will never get a notification and won't be able to comment.") | ||||||
|       }} |       }} | ||||||
|  |  | ||||||
|  | @ -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 |   feature_category :devops_reports | ||||||
| 
 | 
 | ||||||
|  |   urgency :low | ||||||
|  | 
 | ||||||
|   def index |   def index | ||||||
|     @cohorts = load_cohorts |     @cohorts = load_cohorts | ||||||
|     track_cohorts_visit |     track_cohorts_visit | ||||||
|  |  | ||||||
|  | @ -9,6 +9,8 @@ class Admin::DevOpsReportController < Admin::ApplicationController | ||||||
| 
 | 
 | ||||||
|   feature_category :devops_reports |   feature_category :devops_reports | ||||||
| 
 | 
 | ||||||
|  |   urgency :low | ||||||
|  | 
 | ||||||
|   # rubocop: disable CodeReuse/ActiveRecord |   # rubocop: disable CodeReuse/ActiveRecord | ||||||
|   def show |   def show | ||||||
|     @metric = DevOpsReport::Metric.order(:created_at).last&.present |     @metric = DevOpsReport::Metric.order(:created_at).last&.present | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ | ||||||
| class Admin::InstanceReviewController < Admin::ApplicationController | class Admin::InstanceReviewController < Admin::ApplicationController | ||||||
|   feature_category :devops_reports |   feature_category :devops_reports | ||||||
| 
 | 
 | ||||||
|  |   urgency :low | ||||||
|  | 
 | ||||||
|   def index |   def index | ||||||
|     redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}") |     redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}") | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ class Admin::UsageTrendsController < Admin::ApplicationController | ||||||
| 
 | 
 | ||||||
|   feature_category :devops_reports |   feature_category :devops_reports | ||||||
| 
 | 
 | ||||||
|  |   urgency :low | ||||||
|  | 
 | ||||||
|   def index |   def index | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -45,7 +45,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo | ||||||
|     push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml) |     push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml) | ||||||
|     push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml) |     push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml) | ||||||
|     push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml) |     push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml) | ||||||
|     push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml) |  | ||||||
|     push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml) |     push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml) | ||||||
|     # Usage data feature flags |     # Usage data feature flags | ||||||
|     push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml) |     push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml) | ||||||
|  |  | ||||||
|  | @ -16,9 +16,6 @@ class Projects::PipelinesController < Projects::ApplicationController | ||||||
|   before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] |   before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] | ||||||
|   before_action :authorize_update_pipeline!, only: [:retry, :cancel] |   before_action :authorize_update_pipeline!, only: [:retry, :cancel] | ||||||
|   before_action :ensure_pipeline, only: [:show, :downloadable_artifacts] |   before_action :ensure_pipeline, only: [:show, :downloadable_artifacts] | ||||||
|   before_action do |  | ||||||
|     push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml) |  | ||||||
|   end |  | ||||||
| 
 | 
 | ||||||
|   # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 |   # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 | ||||||
|   before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? } |   before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? } | ||||||
|  |  | ||||||
|  | @ -47,6 +47,15 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { | ||||||
|       id |       id | ||||||
|       iid |       iid | ||||||
|       complete |       complete | ||||||
|  |       user { | ||||||
|  |         __typename | ||||||
|  |         id | ||||||
|  |         namespace { | ||||||
|  |           __typename | ||||||
|  |           id | ||||||
|  |           crossProjectPipelineAvailable | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       usesNeeds |       usesNeeds | ||||||
|       userPermissions { |       userPermissions { | ||||||
|         updatePipeline |         updatePipeline | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ module Types | ||||||
|         description(enum_mod.description) if use_description |         description(enum_mod.description) if use_description | ||||||
| 
 | 
 | ||||||
|         enum_mod.definition.each do |key, content| |         enum_mod.definition.each do |key, content| | ||||||
|           value(key.to_s.upcase, **content) |           value(key.to_s.upcase, value: key.to_s, description: content[:description]) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|       # rubocop: enable Graphql/Descriptions |       # rubocop: enable Graphql/Descriptions | ||||||
|  |  | ||||||
|  | @ -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 |     end | ||||||
| 
 | 
 | ||||||
|     # rubocop: disable CodeReuse/ActiveRecord |     # rubocop: disable CodeReuse/ActiveRecord | ||||||
|     def metadata |     def metadata(required_fields = [:issue_count, :total_issue_weight]) | ||||||
|       issuables = item_model.arel_table |       fields = metadata_fields(required_fields) | ||||||
|       keys = metadata_fields.keys |       keys = fields.keys | ||||||
|       # TODO: eliminate need for SQL literal fragment |       # TODO: eliminate need for SQL literal fragment | ||||||
|       columns = Arel.sql(metadata_fields.values_at(*keys).join(', ')) |       columns = Arel.sql(fields.values_at(*keys).join(', ')) | ||||||
|       results = item_model.where(id: init_collection.select(issuables[:id])).pluck(columns) |       results = item_model.where(id: collection_ids) | ||||||
|  |       results = query_additions(results, required_fields) | ||||||
|  |       results = results.select(columns) | ||||||
| 
 | 
 | ||||||
|       Hash[keys.zip(results.flatten)] |       Hash[keys.zip(results.pluck(columns).flatten)] | ||||||
|     end |     end | ||||||
|     # rubocop: enable CodeReuse/ActiveRecord |     # rubocop: enable CodeReuse/ActiveRecord | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def metadata_fields |     # override if needed | ||||||
|       { size: 'COUNT(*)' } |     def query_additions(items, required_fields) | ||||||
|  |       items | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def collection_ids | ||||||
|  |       @collection_ids ||= init_collection.select(item_model.arel_table[:id]) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def metadata_fields(required_fields) | ||||||
|  |       required_fields&.include?(:issue_count) ? { size: 'COUNT(*)' } : {} | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def order(items) |     def order(items) | ||||||
|  |  | ||||||
|  | @ -4,11 +4,13 @@ module IssuableLinks | ||||||
|   class DestroyService < BaseService |   class DestroyService < BaseService | ||||||
|     include IncidentManagement::UsageData |     include IncidentManagement::UsageData | ||||||
| 
 | 
 | ||||||
|     attr_reader :link, :current_user |     attr_reader :link, :current_user, :source, :target | ||||||
| 
 | 
 | ||||||
|     def initialize(link, user) |     def initialize(link, user) | ||||||
|       @link = link |       @link = link | ||||||
|       @current_user = user |       @current_user = user | ||||||
|  |       @source = link.source | ||||||
|  |       @target = link.target | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def execute |     def execute | ||||||
|  | @ -22,6 +24,11 @@ module IssuableLinks | ||||||
| 
 | 
 | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|  |     def create_notes | ||||||
|  |       SystemNoteService.unrelate_issuable(source, target, current_user) | ||||||
|  |       SystemNoteService.unrelate_issuable(target, source, current_user) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     def after_destroy |     def after_destroy | ||||||
|       create_notes |       create_notes | ||||||
|       track_event |       track_event | ||||||
|  |  | ||||||
|  | @ -4,23 +4,10 @@ module IssueLinks | ||||||
|   class DestroyService < IssuableLinks::DestroyService |   class DestroyService < IssuableLinks::DestroyService | ||||||
|     private |     private | ||||||
| 
 | 
 | ||||||
|     def source |  | ||||||
|       @source ||= link.source |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def target |  | ||||||
|       @target ||= link.target |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def permission_to_remove_relation? |     def permission_to_remove_relation? | ||||||
|       can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target) |       can?(current_user, :admin_issue_link, source) && can?(current_user, :admin_issue_link, target) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def create_notes |  | ||||||
|       SystemNoteService.unrelate_issue(source, target, current_user) |  | ||||||
|       SystemNoteService.unrelate_issue(target, source, current_user) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     def track_event |     def track_event | ||||||
|       track_incident_action(current_user, target, :incident_unrelate) |       track_incident_action(current_user, target, :incident_unrelate) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -53,8 +53,8 @@ module SystemNoteService | ||||||
|     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref) |     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def unrelate_issue(noteable, noteable_ref, user) |   def unrelate_issuable(noteable, noteable_ref, user) | ||||||
|     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref) |     ::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issuable(noteable_ref) | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   # Called when the due_date of a Noteable is changed |   # Called when the due_date of a Noteable is changed | ||||||
|  |  | ||||||
|  | @ -26,8 +26,8 @@ module SystemNotes | ||||||
|     #   "removed the relation with gitlab-foss#9001" |     #   "removed the relation with gitlab-foss#9001" | ||||||
|     # |     # | ||||||
|     # Returns the created Note object |     # Returns the created Note object | ||||||
|     def unrelate_issue(noteable_ref) |     def unrelate_issuable(noteable_ref) | ||||||
|       body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}" |       body = "removed the relation with #{noteable_ref.to_reference(noteable.resource_parent)}" | ||||||
| 
 | 
 | ||||||
|       issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue) |       issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -42,10 +42,20 @@ module WebHooks | ||||||
|           hook.failed! |           hook.failed! | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |     rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError | ||||||
|  |       raise if raise_lock_error? | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def lock_name |     def lock_name | ||||||
|       "web_hooks:update_hook_failure_state:#{hook.id}" |       "web_hooks:update_hook_failure_state:#{hook.id}" | ||||||
|     end |     end | ||||||
|  | 
 | ||||||
|  |     # Allow an error to be raised after failing to obtain a lease only if the hook | ||||||
|  |     # is not already in the correct failure state. | ||||||
|  |     def raise_lock_error? | ||||||
|  |       hook.reset # Reload so properties are guaranteed to be current. | ||||||
|  | 
 | ||||||
|  |       hook.executable? != (response_category == :ok) | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -29,4 +29,4 @@ | ||||||
|   #js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } } |   #js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } } | ||||||
|   = render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors |   = render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors | ||||||
| 
 | 
 | ||||||
| .js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } } | .js-pipeline-details-vue{ data: js_pipeline_details_data(@project, @pipeline) } | ||||||
|  |  | ||||||
|  | @ -31,6 +31,7 @@ | ||||||
| - continuous_delivery | - continuous_delivery | ||||||
| - continuous_integration | - continuous_integration | ||||||
| - continuous_integration_scaling | - continuous_integration_scaling | ||||||
|  | - continuous_verification | ||||||
| - database | - database | ||||||
| - dataops | - dataops | ||||||
| - delivery | - delivery | ||||||
|  | @ -74,7 +75,6 @@ | ||||||
| - kubernetes_management | - kubernetes_management | ||||||
| - license | - license | ||||||
| - license_compliance | - license_compliance | ||||||
| - live_preview |  | ||||||
| - logging | - logging | ||||||
| - memory | - memory | ||||||
| - merge_trains | - merge_trains | ||||||
|  | @ -110,7 +110,6 @@ | ||||||
| - secrets_management | - secrets_management | ||||||
| - security_benchmarking | - security_benchmarking | ||||||
| - security_orchestration | - security_orchestration | ||||||
| - self_monitoring |  | ||||||
| - service_desk | - service_desk | ||||||
| - service_ping | - service_ping | ||||||
| - sharding | - sharding | ||||||
|  | @ -119,7 +118,6 @@ | ||||||
| - static_application_security_testing | - static_application_security_testing | ||||||
| - static_site_editor | - static_site_editor | ||||||
| - subgroups | - subgroups | ||||||
| - synthetic_monitoring |  | ||||||
| - team_planning | - team_planning | ||||||
| - tracing | - tracing | ||||||
| - usage_ping | - usage_ping | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| --- | --- | ||||||
| name: ci_pending_builds_maintain_denormalized_data | name: ci_pending_builds_maintain_denormalized_data | ||||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75425 | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75425 | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951 | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496 | ||||||
| milestone: '14.6' | milestone: '14.6' | ||||||
| type: development | type: development | ||||||
| group: group::pipeline execution | group: group::pipeline execution | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| --- | --- | ||||||
| name: ci_pending_builds_queue_source | name: ci_pending_builds_queue_source | ||||||
| introduced_by_url: | introduced_by_url: | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350884 | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496 | ||||||
| milestone: '14.0' | milestone: '14.0' | ||||||
| type: development | type: development | ||||||
| group: group::pipeline execution | group: group::pipeline execution | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| --- | --- | ||||||
| name: ci_queuing_use_denormalized_data_strategy | name: ci_queuing_use_denormalized_data_strategy | ||||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76543 | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76543 | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951 | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354496 | ||||||
| milestone: '14.6' | milestone: '14.6' | ||||||
| type: development | type: development | ||||||
| group: group::pipeline execution | group: group::pipeline execution | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| --- | --- | ||||||
| name: rearrange_pipelines_table | name: container_registry_follow_redirects_middleware | ||||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72545 | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81056 | ||||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343286 | rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353291 | ||||||
| milestone: '14.8' | milestone: '14.9' | ||||||
| type: development | type: development | ||||||
| group: group::pipeline execution | group: group::package | ||||||
| default_enabled: true | default_enabled: false | ||||||
|  | @ -62,3 +62,27 @@ | ||||||
|     - 'i_testing_group_code_coverage_visit_total' |     - 'i_testing_group_code_coverage_visit_total' | ||||||
|     - 'i_testing_load_performance_widget_total' |     - 'i_testing_load_performance_widget_total' | ||||||
|     - 'i_testing_metrics_report_widget_total' |     - 'i_testing_metrics_report_widget_total' | ||||||
|  | - name: xmau_plan | ||||||
|  |   operator: OR | ||||||
|  |   source: redis | ||||||
|  |   time_frame: [7d, 28d] | ||||||
|  |   events: | ||||||
|  |     - users_creating_work_items | ||||||
|  |     - users_updating_work_item_title | ||||||
|  |   feature_flag: track_work_items_activity | ||||||
|  | - name: xmau_project_management | ||||||
|  |   operator: OR | ||||||
|  |   source: redis | ||||||
|  |   time_frame: [7d, 28d] | ||||||
|  |   events: | ||||||
|  |     - users_creating_work_items | ||||||
|  |     - users_updating_work_item_title | ||||||
|  |   feature_flag: track_work_items_activity | ||||||
|  | - name: users_work_items | ||||||
|  |   operator: OR | ||||||
|  |   source: redis | ||||||
|  |   time_frame: [7d, 28d] | ||||||
|  |   events: | ||||||
|  |     - users_creating_work_items | ||||||
|  |     - users_updating_work_item_title | ||||||
|  |   feature_flag: track_work_items_activity | ||||||
|  |  | ||||||
|  | @ -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_milestone: "14.8"  # The milestone when this feature was first announced as deprecated. | ||||||
|   announcement_date: "2022-02-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   announcement_date: "2022-02-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed |   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed | ||||||
|   removal_date: 2022-05-22 # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   removal_date: "2022-05-22"  # The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   breaking_change: true  # If this deprecation is a breaking change, set this value to true |   breaking_change: true  # If this deprecation is a breaking change, set this value to true | ||||||
|   reporter: jacobvosmaer-gitlab  # GitLab username of the person reporting the deprecation |   reporter: jacobvosmaer-gitlab  # GitLab username of the person reporting the deprecation | ||||||
|   body: |  # Do not modify this line, instead modify the lines below. |   body: |  # Do not modify this line, instead modify the lines below. | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|   announcement_milestone: "14.8"  # The milestone when this feature was first announced as deprecated. |   announcement_milestone: "14.8"  # The milestone when this feature was first announced as deprecated. | ||||||
|   announcement_date: "2021-02-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   announcement_date: "2021-02-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed |   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed | ||||||
|   removal_date: 2021-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   removal_date: "2021-05-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   breaking_change: true  # If this deprecation is a breaking change, set this value to true |   breaking_change: true  # If this deprecation is a breaking change, set this value to true | ||||||
|   body: |  # Do not modify this line, instead modify the lines below. |   body: |  # Do not modify this line, instead modify the lines below. | ||||||
|     For those using Dependency Scanning for Python projects, we are deprecating the default `gemnasium-python:2` image which uses Python 3.6 as well as the custom `gemnasium-python:2-python-3.9` image which uses Python 3.9. The new default image as of GitLab 15.0 will be for Python 3.9 as it is a [supported version](https://endoflife.date/python) and 3.6 [is no longer supported](https://endoflife.date/python). |     For those using Dependency Scanning for Python projects, we are deprecating the default `gemnasium-python:2` image which uses Python 3.6 as well as the custom `gemnasium-python:2-python-3.9` image which uses Python 3.9. The new default image as of GitLab 15.0 will be for Python 3.9 as it is a [supported version](https://endoflife.date/python) and 3.6 [is no longer supported](https://endoflife.date/python). | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|   announcement_milestone: "14.8"  # The milestone when this feature was first announced as deprecated. |   announcement_milestone: "14.8"  # The milestone when this feature was first announced as deprecated. | ||||||
|   announcement_date: "2022-02-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   announcement_date: "2022-02-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed |   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed | ||||||
|   removal_date: 2022-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   removal_date: "2022-05-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   breaking_change: true  # If this deprecation is a breaking change, set this value to true |   breaking_change: true  # If this deprecation is a breaking change, set this value to true | ||||||
|   body: |  # Do not modify this line, instead modify the lines below. |   body: |  # Do not modify this line, instead modify the lines below. | ||||||
|     By default, all new applications expire access tokens after 2 hours. In GitLab 14.2 and earlier, OAuth access tokens |     By default, all new applications expire access tokens after 2 hours. In GitLab 14.2 and earlier, OAuth access tokens | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|   announcement_milestone: "14.0"  # The milestone when this feature was first announced as deprecated. |   announcement_milestone: "14.0"  # The milestone when this feature was first announced as deprecated. | ||||||
|   announcement_date: "2021-06-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   announcement_date: "2021-06-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed |   removal_milestone: "15.0"  # The milestone when this feature is planned to be removed | ||||||
|   removal_date: 2022-05-22 # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. |   removal_date: "2022-05-22"  # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post. | ||||||
|   breaking_change: yes # If this deprecation is a breaking change, set this value to true |   breaking_change: yes # If this deprecation is a breaking change, set this value to true | ||||||
|   body: |  # Do not modify this line, instead modify the lines below. |   body: |  # Do not modify this line, instead modify the lines below. | ||||||
|     The OAuth implicit grant authorization flow will be removed in our next major release, GitLab 15.0. Any applications that use OAuth implicit grant should switch to alternative [supported OAuth flows](https://docs.gitlab.com/ee/api/oauth2.html). |     The OAuth implicit grant authorization flow will be removed in our next major release, GitLab 15.0. Any applications that use OAuth implicit grant should switch to alternative [supported OAuth flows](https://docs.gitlab.com/ee/api/oauth2.html). | ||||||
|  |  | ||||||
|  | @ -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_duplicated_to_id ON issues USING btree (duplicated_to_id) WHERE (duplicated_to_id IS NOT NULL); | ||||||
| 
 | 
 | ||||||
|  | CREATE INDEX index_issues_on_id_and_weight ON issues USING btree (id, weight); | ||||||
|  | 
 | ||||||
| CREATE INDEX index_issues_on_incident_issue_type ON issues USING btree (issue_type) WHERE (issue_type = 1); | CREATE INDEX index_issues_on_incident_issue_type ON issues USING btree (issue_type) WHERE (issue_type = 1); | ||||||
| 
 | 
 | ||||||
| CREATE INDEX index_issues_on_last_edited_by_id ON issues USING btree (last_edited_by_id); | CREATE INDEX index_issues_on_last_edited_by_id ON issues USING btree (last_edited_by_id); | ||||||
|  |  | ||||||
|  | @ -208,13 +208,18 @@ When the number exceeds the limit the page displays an alert and links to a pagi | ||||||
| 
 | 
 | ||||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51401) in GitLab 11.10. | > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51401) in GitLab 11.10. | ||||||
| 
 | 
 | ||||||
| The number of pipelines that can be created in a single push is 4. | When pushing multiple changes with a single Git push, like multiple tags or branches, | ||||||
| This limit prevents the accidental creation of pipelines when `git push --all` | only four tag or branch pipelines can be triggered. This limit prevents the accidental | ||||||
| or `git push --mirror` is used. | creation of a large number of pipelines when using `git push --all` or `git push --mirror`. | ||||||
| 
 | 
 | ||||||
| This limit does not affect any of the updated merge request pipelines. | [Merge request pipelines](../ci/pipelines/merge_request_pipelines.md) are not limited. | ||||||
| All updated merge requests have a pipeline created when using | If the Git push updates multiple merge requests at the same time, a merge request pipeline | ||||||
| [merge request pipelines](../ci/pipelines/merge_request_pipelines.md). | can trigger for every updated merge request. | ||||||
|  | 
 | ||||||
|  | To remove the limit so that any number of pipelines can trigger for a single Git push event, | ||||||
|  | administrators can enable the `git_push_create_all_pipelines` [feature flag](feature_flags.md). | ||||||
|  | Enabling this feature flag is not recommended, as it can cause excessive load on the GitLab | ||||||
|  | instance if too many changes are pushed at once and a flood of pipelines are created accidentally. | ||||||
| 
 | 
 | ||||||
| ## Retention of activity history | ## Retention of activity history | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10759,10 +10759,11 @@ Represents an epic board list. | ||||||
| | Name | Type | Description | | | Name | Type | Description | | ||||||
| | ---- | ---- | ----------- | | | ---- | ---- | ----------- | | ||||||
| | <a id="epiclistcollapsed"></a>`collapsed` | [`Boolean`](#boolean) | Indicates if this list is collapsed for this user. | | | <a id="epiclistcollapsed"></a>`collapsed` | [`Boolean`](#boolean) | Indicates if this list is collapsed for this user. | | ||||||
| | <a id="epiclistepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. | | | <a id="epiclistepicscount"></a>`epicsCount` **{warning-solid}** | [`Int`](#int) | **Deprecated** in 14.9. This was renamed. Use: `metadata`. | | ||||||
| | <a id="epiclistid"></a>`id` | [`BoardsEpicListID!`](#boardsepiclistid) | Global ID of the board list. | | | <a id="epiclistid"></a>`id` | [`BoardsEpicListID!`](#boardsepiclistid) | Global ID of the board list. | | ||||||
| | <a id="epiclistlabel"></a>`label` | [`Label`](#label) | Label of the list. | | | <a id="epiclistlabel"></a>`label` | [`Label`](#label) | Label of the list. | | ||||||
| | <a id="epiclistlisttype"></a>`listType` | [`String!`](#string) | Type of the list. | | | <a id="epiclistlisttype"></a>`listType` | [`String!`](#string) | Type of the list. | | ||||||
|  | | <a id="epiclistmetadata"></a>`metadata` | [`EpicListMetadata`](#epiclistmetadata) | Epic list metatada. | | ||||||
| | <a id="epiclistposition"></a>`position` | [`Int`](#int) | Position of the list within the board. | | | <a id="epiclistposition"></a>`position` | [`Int`](#int) | Position of the list within the board. | | ||||||
| | <a id="epiclisttitle"></a>`title` | [`String!`](#string) | Title of the list. | | | <a id="epiclisttitle"></a>`title` | [`String!`](#string) | Title of the list. | | ||||||
| 
 | 
 | ||||||
|  | @ -10784,6 +10785,17 @@ four standard [pagination arguments](#connection-pagination-arguments): | ||||||
| | ---- | ---- | ----------- | | | ---- | ---- | ----------- | | ||||||
| | <a id="epiclistepicsfilters"></a>`filters` | [`EpicFilters`](#epicfilters) | Filters applied when selecting epics in the board list. | | | <a id="epiclistepicsfilters"></a>`filters` | [`EpicFilters`](#epicfilters) | Filters applied when selecting epics in the board list. | | ||||||
| 
 | 
 | ||||||
|  | ### `EpicListMetadata` | ||||||
|  | 
 | ||||||
|  | Represents epic board list metadata. | ||||||
|  | 
 | ||||||
|  | #### Fields | ||||||
|  | 
 | ||||||
|  | | Name | Type | Description | | ||||||
|  | | ---- | ---- | ----------- | | ||||||
|  | | <a id="epiclistmetadataepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. | | ||||||
|  | | <a id="epiclistmetadatatotalweight"></a>`totalWeight` | [`Int`](#int) | Total weight of all issues in the list. Available only when feature flag `epic_board_total_weight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | | ||||||
|  | 
 | ||||||
| ### `EpicPermissions` | ### `EpicPermissions` | ||||||
| 
 | 
 | ||||||
| Check permissions for the current user on an epic. | Check permissions for the current user on an epic. | ||||||
|  | @ -15271,6 +15283,7 @@ Represents the security scan information. | ||||||
| | ---- | ---- | ----------- | | | ---- | ---- | ----------- | | ||||||
| | <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. | | | <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. | | ||||||
| | <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. | | | <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. | | ||||||
|  | | <a id="scanstatus"></a>`status` | [`ScanStatus!`](#scanstatus) | Indicates the status of the scan. | | ||||||
| | <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. | | | <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. | | ||||||
| 
 | 
 | ||||||
| ### `ScanExecutionPolicy` | ### `ScanExecutionPolicy` | ||||||
|  | @ -18142,6 +18155,20 @@ Size of UI component in SAST configuration page. | ||||||
| | <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. | | | <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. | | ||||||
| | <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. | | | <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. | | ||||||
| 
 | 
 | ||||||
|  | ### `ScanStatus` | ||||||
|  | 
 | ||||||
|  | The status of the security scan. | ||||||
|  | 
 | ||||||
|  | | Value | Description | | ||||||
|  | | ----- | ----------- | | ||||||
|  | | <a id="scanstatuscreated"></a>`CREATED` | The scan has been created. | | ||||||
|  | | <a id="scanstatusjob_failed"></a>`JOB_FAILED` | The related CI build failed. | | ||||||
|  | | <a id="scanstatuspreparation_failed"></a>`PREPARATION_FAILED` | Report couldn't be prepared. | | ||||||
|  | | <a id="scanstatuspreparing"></a>`PREPARING` | Preparing the report for the scan. | | ||||||
|  | | <a id="scanstatuspurged"></a>`PURGED` | Report for the scan has been removed from the database. | | ||||||
|  | | <a id="scanstatusreport_error"></a>`REPORT_ERROR` | The report artifact provided by the CI build couldn't be parsed. | | ||||||
|  | | <a id="scanstatussucceeded"></a>`SUCCEEDED` | The report has been successfully prepared. | | ||||||
|  | 
 | ||||||
| ### `SecurityReportTypeEnum` | ### `SecurityReportTypeEnum` | ||||||
| 
 | 
 | ||||||
| | Value | Description | | | Value | Description | | ||||||
|  |  | ||||||
|  | @ -12,9 +12,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w | ||||||
| > - It's enabled on GitLab.com. | > - It's enabled on GitLab.com. | ||||||
| > - It's recommended for production use. | > - It's recommended for production use. | ||||||
| 
 | 
 | ||||||
| WARNING: |  | ||||||
| This feature might not be available to you. Check the **version history** note above for details. |  | ||||||
| 
 |  | ||||||
| Usage Trends gives you an overview of how much data your instance contains, and how quickly this volume is changing over time. | Usage Trends gives you an overview of how much data your instance contains, and how quickly this volume is changing over time. | ||||||
| Usage Trends data refreshes daily. | Usage Trends data refreshes daily. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -219,7 +219,7 @@ security issues: | ||||||
| WARNING: | WARNING: | ||||||
| This feature is in its end-of-life process. It is [deprecated](../../update/deprecations.md#vulnerability-check) | This feature is in its end-of-life process. It is [deprecated](../../update/deprecations.md#vulnerability-check) | ||||||
| for use in GitLab 14.8, and is planned for removal in GitLab 15.0. Users should migrate to the new | for use in GitLab 14.8, and is planned for removal in GitLab 15.0. Users should migrate to the new | ||||||
| [Security Approval Policies](policies/#scan-result-policy-editor). | [Security Approval Policies](policies/scan-result-policies.md). | ||||||
| 
 | 
 | ||||||
| To prevent a merge request introducing a security vulnerability in a project, enable the | To prevent a merge request introducing a security vulnerability in a project, enable the | ||||||
| Vulnerability-Check rule. While this rule is enabled, additional merge request approval by | Vulnerability-Check rule. While this rule is enabled, additional merge request approval by | ||||||
|  |  | ||||||
|  | @ -12,16 +12,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w | ||||||
| > - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) to GitLab Free in 14.5. | > - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) to GitLab Free in 14.5. | ||||||
| > - Support for Omnibus installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5. | > - Support for Omnibus installations was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5686) in GitLab 14.5. | ||||||
| 
 | 
 | ||||||
| You can use GitLab CI/CD to safely deploy to and update your Kubernetes clusters. | You can use a GitLab CI/CD workflow to safely deploy to and update your Kubernetes clusters. | ||||||
| 
 | 
 | ||||||
| To do so, you install a GitLab agent in your cluster. Then in your GitLab CI/CD pipelines, | To do so, you must first [install an agent in your cluster](install/index.md). When done, you have a Kubernetes context and can | ||||||
| you can refer to the cluster connection as a Kubernetes context. | run Kubernetes API commands in your GitLab CI/CD pipeline. | ||||||
| Then you can run Kubernetes API commands as part of your GitLab CI/CD pipeline. |  | ||||||
| 
 | 
 | ||||||
| To ensure access to your cluster is safe: | To ensure access to your cluster is safe: | ||||||
| 
 | 
 | ||||||
| - Each agent has a separate context (`kubecontext`). | - Each agent has a separate context (`kubecontext`). | ||||||
| - Only the project where the agent is, and any additional projects you authorize can access the agent in your cluster. | - Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster. | ||||||
| 
 | 
 | ||||||
| You do not need to have a runner in the cluster with the agent. | You do not need to have a runner in the cluster with the agent. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,22 @@ info: To determine the technical writer assigned to the Stage/Group associated w | ||||||
| > - [Renamed](https://gitlab.com/groups/gitlab-org/-/epics/7167) from "GitLab Kubernetes Agent" to "GitLab agent for Kubernetes" in GitLab 14.6. | > - [Renamed](https://gitlab.com/groups/gitlab-org/-/epics/7167) from "GitLab Kubernetes Agent" to "GitLab agent for Kubernetes" in GitLab 14.6. | ||||||
| 
 | 
 | ||||||
| You can connect your Kubernetes cluster with GitLab to deploy, manage, | You can connect your Kubernetes cluster with GitLab to deploy, manage, | ||||||
| and monitor your cloud-native solutions. You can choose from two primary workflows. | and monitor your cloud-native solutions.  | ||||||
|  | 
 | ||||||
|  | To connect a Kubernetes cluster to GitLab, you must first [install an agent in your cluster](install/index.md). | ||||||
|  | 
 | ||||||
|  | The agent runs in the cluster, and you can use it to: | ||||||
|  | 
 | ||||||
|  | - Communicate with a cluster, which is behind a firewall or NAT. | ||||||
|  | - Access API endpoints in a cluster in real time. | ||||||
|  | - Push information about events happening in the cluster. | ||||||
|  | - Enable a cache of Kubernetes objects, which are kept up-to-date with very low latency. | ||||||
|  | 
 | ||||||
|  | For more details about the agent's purpose and architecture, see the [architecture documentation](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/architecture.md). | ||||||
|  | 
 | ||||||
|  | ## Workflows | ||||||
|  | 
 | ||||||
|  | You can choose from two primary workflows. | ||||||
| 
 | 
 | ||||||
| In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and | In a [**GitOps** workflow](gitops.md), you keep your Kubernetes manifests in GitLab. You install a GitLab agent in your cluster, and | ||||||
| any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based, | any time you update your manifests, the agent updates the cluster. This workflow is fully driven with Git and is considered pull-based, | ||||||
|  | @ -23,8 +38,6 @@ because the cluster is pulling updates from your GitLab repository. | ||||||
| In a [**CI/CD** workflow](ci_cd_tunnel.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API. | In a [**CI/CD** workflow](ci_cd_tunnel.md), you use GitLab CI/CD to query and update your cluster by using the Kubernetes API. | ||||||
| This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster. | This workflow is considered push-based, because GitLab is pushing requests from GitLab CI/CD to your cluster. | ||||||
| 
 | 
 | ||||||
| Both of these workflows require you to [install an agent in your cluster](install/index.md). |  | ||||||
| 
 |  | ||||||
| ## Supported cluster versions | ## Supported cluster versions | ||||||
| 
 | 
 | ||||||
| GitLab supports the following Kubernetes versions. You can upgrade your | GitLab supports the following Kubernetes versions. You can upgrade your | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ module API | ||||||
|     desc 'Get the current application statistics' do |     desc 'Get the current application statistics' do | ||||||
|       success Entities::ApplicationStatistics |       success Entities::ApplicationStatistics | ||||||
|     end |     end | ||||||
|     get "application/statistics" do |     get "application/statistics", urgency: :low do | ||||||
|       counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS) |       counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS) | ||||||
|       present counts, with: Entities::ApplicationStatistics |       present counts, with: Entities::ApplicationStatistics | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -10,6 +10,21 @@ module ContainerRegistry | ||||||
|     REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features' |     REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features' | ||||||
|     REGISTRY_TAG_DELETE_FEATURE = 'tag_delete' |     REGISTRY_TAG_DELETE_FEATURE = 'tag_delete' | ||||||
| 
 | 
 | ||||||
|  |     ALLOWED_REDIRECT_SCHEMES = %w[http https].freeze | ||||||
|  |     REDIRECT_OPTIONS = { | ||||||
|  |       clear_authorization_header: true, | ||||||
|  |       limit: 3, | ||||||
|  |       cookies: [], | ||||||
|  |       callback: -> (response_env, request_env) do | ||||||
|  |         request_env.request_headers.delete(::FaradayMiddleware::FollowRedirects::AUTH_HEADER) | ||||||
|  | 
 | ||||||
|  |         redirect_to = request_env.url | ||||||
|  |         unless redirect_to.scheme.in?(ALLOWED_REDIRECT_SCHEMES) | ||||||
|  |           raise ArgumentError, "Invalid scheme for #{redirect_to}" | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     }.freeze | ||||||
|  | 
 | ||||||
|     def self.supports_tag_delete? |     def self.supports_tag_delete? | ||||||
|       with_dummy_client(return_value_if_disabled: false) do |client| |       with_dummy_client(return_value_if_disabled: false) do |client| | ||||||
|         client.supports_tag_delete? |         client.supports_tag_delete? | ||||||
|  | @ -136,6 +151,10 @@ module ContainerRegistry | ||||||
|     def faraday_blob |     def faraday_blob | ||||||
|       @faraday_blob ||= faraday_base do |conn| |       @faraday_blob ||= faraday_base do |conn| | ||||||
|         initialize_connection(conn, @options) |         initialize_connection(conn, @options) | ||||||
|  | 
 | ||||||
|  |         if Feature.enabled?(:container_registry_follow_redirects_middleware) | ||||||
|  |           conn.use ::FaradayMiddleware::FollowRedirects, REDIRECT_OPTIONS | ||||||
|  |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -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}" | msgid "GithubIntegration|This requires mirroring your GitHub repository to this project. %{docs_link}" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "GitlabTiers|Free" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "GitlabTiers|Premium" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "GitlabTiers|Ultimate" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Gitpod" | msgid "Gitpod" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -26990,6 +26999,9 @@ msgstr "" | ||||||
| msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners." | msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Pipelines|Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines. %{linkStart}Learn More%{linkEnd}" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you." | msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -27029,6 +27041,9 @@ msgstr "" | ||||||
| msgid "Pipelines|More Information" | msgid "Pipelines|More Information" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Pipelines|Multi-project pipeline graphs" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Pipelines|No runners detected" | msgid "Pipelines|No runners detected" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -27179,9 +27194,6 @@ msgstr "" | ||||||
| msgid "Pipeline|Checking pipeline status." | msgid "Pipeline|Checking pipeline status." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "Pipeline|Commit" |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}." | msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -27197,9 +27209,6 @@ msgstr "" | ||||||
| msgid "Pipeline|Detached merge request pipeline" | msgid "Pipeline|Detached merge request pipeline" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "Pipeline|Duration" |  | ||||||
| msgstr "" |  | ||||||
| 
 |  | ||||||
| msgid "Pipeline|Failed" | msgid "Pipeline|Failed" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -27743,6 +27752,9 @@ msgstr "" | ||||||
| msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}." | msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Preparing the report for the scan." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Prev" | msgid "Prev" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -30798,6 +30810,12 @@ msgstr "" | ||||||
| msgid "Report abuse to admin" | msgid "Report abuse to admin" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Report couldn't be prepared." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "Report for the scan has been removed from the database." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Reported %{timeAgo} by %{reportedBy}" | msgid "Reported %{timeAgo} by %{reportedBy}" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -36831,6 +36849,9 @@ msgstr "" | ||||||
| msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable." | msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "The related CI build failed." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "The remote mirror URL is invalid." | msgid "The remote mirror URL is invalid." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -36840,6 +36861,12 @@ msgstr "" | ||||||
| msgid "The remote repository is being updated..." | msgid "The remote repository is being updated..." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "The report artifact provided by the CI build couldn't be parsed." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "The report has been successfully prepared." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "The repository can be committed to, and issues, comments and other entities can be created." | msgid "The repository can be committed to, and issues, comments and other entities can be created." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -36861,6 +36888,9 @@ msgstr "" | ||||||
| msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)." | msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "The scan has been created." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "The snippet can be accessed without any authentication." | msgid "The snippet can be accessed without any authentication." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -37314,7 +37344,7 @@ msgstr "" | ||||||
| msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment." | msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}." | msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
| msgid "This %{noteableTypeText} is locked." | msgid "This %{noteableTypeText} is locked." | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ module QA | ||||||
|           view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do |           view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do | ||||||
|             element :expand_pipeline_button |             element :expand_pipeline_button | ||||||
|             element :child_pipeline |             element :child_pipeline | ||||||
|  |             element :linked_pipeline_body | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           view 'app/assets/javascripts/reports/components/report_section.vue' do |           view 'app/assets/javascripts/reports/components/report_section.vue' do | ||||||
|  | @ -93,7 +94,7 @@ module QA | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def find_child_pipeline_by_title(title) |           def find_child_pipeline_by_title(title) | ||||||
|             child_pipelines.find { |pipeline| pipeline[:title].include?(title) } |             find_element(:child_pipeline, text: title) | ||||||
|           end |           end | ||||||
| 
 | 
 | ||||||
|           def expand_child_pipeline(title: nil) |           def expand_child_pipeline(title: nil) | ||||||
|  |  | ||||||
|  | @ -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 |             end | ||||||
| 
 | 
 | ||||||
|             Page::Project::Job::Show.perform do |show| |             Page::Project::Job::Show.perform do |show| | ||||||
|               expect(show).to have_passed(timeout: 360) |               expect(show).to have_passed(timeout: 800) | ||||||
|             end |             end | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -125,7 +125,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do | ||||||
| 
 | 
 | ||||||
|       before do |       before do | ||||||
|         stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false) |         stub_feature_flags(ci_disallow_to_create_merge_request_pipelines_in_target_project: false) | ||||||
|         stub_feature_flags(rearrange_pipelines_table: false) |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'creates a pipeline in the parent project when user proceeds with the warning' do |       it 'creates a pipeline in the parent project when user proceeds with the warning' do | ||||||
|  |  | ||||||
|  | @ -161,7 +161,7 @@ RSpec.describe 'Pipelines', :js do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned off' do |       context 'when pipeline is detached merge request pipeline' do | ||||||
|         let(:merge_request) do |         let(:merge_request) do | ||||||
|           create(:merge_request, |           create(:merge_request, | ||||||
|                  :with_detached_merge_request_pipeline, |                  :with_detached_merge_request_pipeline, | ||||||
|  | @ -174,52 +174,6 @@ RSpec.describe 'Pipelines', :js do | ||||||
|         let(:target_project) { project } |         let(:target_project) { project } | ||||||
| 
 | 
 | ||||||
|         before do |         before do | ||||||
|           stub_feature_flags(rearrange_pipelines_table: false) |  | ||||||
| 
 |  | ||||||
|           visit project_pipelines_path(source_project) |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         shared_examples_for 'detached merge request pipeline' do |  | ||||||
|           it 'shows pipeline information without pipeline ref', :sidekiq_might_not_need_inline do |  | ||||||
|             within '.pipeline-tags' do |  | ||||||
|               expect(page).to have_content(expected_detached_mr_tag) |  | ||||||
|             end |  | ||||||
| 
 |  | ||||||
|             within '.branch-commit' do |  | ||||||
|               expect(page).to have_link(merge_request.iid, |  | ||||||
|                 href: project_merge_request_path(project, merge_request)) |  | ||||||
|             end |  | ||||||
| 
 |  | ||||||
|             within '.branch-commit' do |  | ||||||
|               expect(page).not_to have_link(pipeline.ref) |  | ||||||
|             end |  | ||||||
|           end |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it_behaves_like 'detached merge request pipeline' |  | ||||||
| 
 |  | ||||||
|         context 'when source project is a forked project' do |  | ||||||
|           let(:source_project) { fork_project(project, user, repository: true) } |  | ||||||
| 
 |  | ||||||
|           it_behaves_like 'detached merge request pipeline' |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'when pipeline is detached merge request pipeline, with rearrange_pipelines_table feature flag turned on' do |  | ||||||
|         let(:merge_request) do |  | ||||||
|           create(:merge_request, |  | ||||||
|                  :with_detached_merge_request_pipeline, |  | ||||||
|                  source_project: source_project, |  | ||||||
|                  target_project: target_project) |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         let!(:pipeline) { merge_request.all_pipelines.first } |  | ||||||
|         let(:source_project) { project } |  | ||||||
|         let(:target_project) { project } |  | ||||||
| 
 |  | ||||||
|         before do |  | ||||||
|           stub_feature_flags(rearrange_pipelines_table: true) |  | ||||||
| 
 |  | ||||||
|           visit project_pipelines_path(source_project) |           visit project_pipelines_path(source_project) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -245,7 +199,7 @@ RSpec.describe 'Pipelines', :js do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned off' do |       context 'when pipeline is merge request pipeline' do | ||||||
|         let(:merge_request) do |         let(:merge_request) do | ||||||
|           create(:merge_request, |           create(:merge_request, | ||||||
|                  :with_merge_request_pipeline, |                  :with_merge_request_pipeline, | ||||||
|  | @ -259,53 +213,6 @@ RSpec.describe 'Pipelines', :js do | ||||||
|         let(:target_project) { project } |         let(:target_project) { project } | ||||||
| 
 | 
 | ||||||
|         before do |         before do | ||||||
|           stub_feature_flags(rearrange_pipelines_table: false) |  | ||||||
| 
 |  | ||||||
|           visit project_pipelines_path(source_project) |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         shared_examples_for 'Correct merge request pipeline information' do |  | ||||||
|           it 'does not show detached tag for the pipeline, and shows the link of the merge request, and does not show the ref of the pipeline', :sidekiq_might_not_need_inline do |  | ||||||
|             within '.pipeline-tags' do |  | ||||||
|               expect(page).not_to have_content(expected_detached_mr_tag) |  | ||||||
|             end |  | ||||||
| 
 |  | ||||||
|             within '.branch-commit' do |  | ||||||
|               expect(page).to have_link(merge_request.iid, |  | ||||||
|                 href: project_merge_request_path(project, merge_request)) |  | ||||||
|             end |  | ||||||
| 
 |  | ||||||
|             within '.branch-commit' do |  | ||||||
|               expect(page).not_to have_link(pipeline.ref) |  | ||||||
|             end |  | ||||||
|           end |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it_behaves_like 'Correct merge request pipeline information' |  | ||||||
| 
 |  | ||||||
|         context 'when source project is a forked project' do |  | ||||||
|           let(:source_project) { fork_project(project, user, repository: true) } |  | ||||||
| 
 |  | ||||||
|           it_behaves_like 'Correct merge request pipeline information' |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'when pipeline is merge request pipeline, with rearrange_pipelines_table feature flag turned on' do |  | ||||||
|         let(:merge_request) do |  | ||||||
|           create(:merge_request, |  | ||||||
|                  :with_merge_request_pipeline, |  | ||||||
|                  source_project: source_project, |  | ||||||
|                  target_project: target_project, |  | ||||||
|                  merge_sha: target_project.commit.sha) |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         let!(:pipeline) { merge_request.all_pipelines.first } |  | ||||||
|         let(:source_project) { project } |  | ||||||
|         let(:target_project) { project } |  | ||||||
| 
 |  | ||||||
|         before do |  | ||||||
|           stub_feature_flags(rearrange_pipelines_table: true) |  | ||||||
| 
 |  | ||||||
|           visit project_pipelines_path(source_project) |           visit project_pipelines_path(source_project) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -676,28 +583,6 @@ RSpec.describe 'Pipelines', :js do | ||||||
| 
 | 
 | ||||||
|       context 'with pipeline key selection' do |       context 'with pipeline key selection' do | ||||||
|         before do |         before do | ||||||
|           stub_feature_flags(rearrange_pipelines_table: false) |  | ||||||
|           visit project_pipelines_path(project) |  | ||||||
|           wait_for_requests |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it 'changes the Pipeline ID column for Pipeline IID' do |  | ||||||
|           page.find('[data-testid="pipeline-key-dropdown"]').click |  | ||||||
| 
 |  | ||||||
|           within '.gl-new-dropdown-contents' do |  | ||||||
|             dropdown_options = page.find_all '.gl-new-dropdown-item' |  | ||||||
| 
 |  | ||||||
|             dropdown_options[1].click |  | ||||||
|           end |  | ||||||
| 
 |  | ||||||
|           expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline IID' |  | ||||||
|           expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}" |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'with pipeline key selection and rearrange_pipelines_table ff on' do |  | ||||||
|         before do |  | ||||||
|           stub_feature_flags(rearrange_pipelines_table: true) |  | ||||||
|           visit project_pipelines_path(project) |           visit project_pipelines_path(project) | ||||||
|           wait_for_requests |           wait_for_requests | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -93,5 +93,38 @@ describe('DirtySubmitForm', () => { | ||||||
| 
 | 
 | ||||||
|       expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length); |       expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     describe('when inputs listener is added', () => { | ||||||
|  |       it('calls listener when changes are made to an input', () => { | ||||||
|  |         const { form, input } = createForm(); | ||||||
|  |         const inputsListener = jest.fn(); | ||||||
|  | 
 | ||||||
|  |         const dirtySubmitForm = new DirtySubmitForm(form); | ||||||
|  |         dirtySubmitForm.addInputsListener(inputsListener); | ||||||
|  | 
 | ||||||
|  |         setInputValue(input, 'new value'); | ||||||
|  | 
 | ||||||
|  |         jest.runOnlyPendingTimers(); | ||||||
|  | 
 | ||||||
|  |         expect(inputsListener).toHaveBeenCalledTimes(1); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       describe('when inputs listener is removed', () => { | ||||||
|  |         it('does not call listener when changes are made to an input', () => { | ||||||
|  |           const { form, input } = createForm(); | ||||||
|  |           const inputsListener = jest.fn(); | ||||||
|  | 
 | ||||||
|  |           const dirtySubmitForm = new DirtySubmitForm(form); | ||||||
|  |           dirtySubmitForm.addInputsListener(inputsListener); | ||||||
|  |           dirtySubmitForm.removeInputsListener(inputsListener); | ||||||
|  | 
 | ||||||
|  |           setInputValue(input, 'new value'); | ||||||
|  | 
 | ||||||
|  |           jest.runOnlyPendingTimers(); | ||||||
|  | 
 | ||||||
|  |           expect(inputsListener).not.toHaveBeenCalled(); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { GlIntersectionObserver, GlSkeletonLoader } from '@gitlab/ui'; | import { GlIntersectionObserver, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui'; | ||||||
| import { shallowMount } from '@vue/test-utils'; | import { shallowMount } from '@vue/test-utils'; | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import VueApollo from 'vue-apollo'; | import VueApollo from 'vue-apollo'; | ||||||
|  | @ -19,6 +19,7 @@ describe('Jobs app', () => { | ||||||
|   let resolverSpy; |   let resolverSpy; | ||||||
| 
 | 
 | ||||||
|   const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); |   const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); | ||||||
|  |   const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon); | ||||||
|   const findJobsTable = () => wrapper.findComponent(JobsTable); |   const findJobsTable = () => wrapper.findComponent(JobsTable); | ||||||
| 
 | 
 | ||||||
|   const triggerInfiniteScroll = () => |   const triggerInfiniteScroll = () => | ||||||
|  | @ -48,7 +49,29 @@ describe('Jobs app', () => { | ||||||
|     wrapper.destroy(); |     wrapper.destroy(); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('displays the loading state', () => { |   describe('loading spinner', () => { | ||||||
|  |     beforeEach(async () => { | ||||||
|  |       createComponent(resolverSpy); | ||||||
|  | 
 | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       triggerInfiniteScroll(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('displays loading spinner when fetching more jobs', () => { | ||||||
|  |       expect(findLoadingSpinner().exists()).toBe(true); | ||||||
|  |       expect(findSkeletonLoader().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('hides loading spinner after jobs have been fetched', async () => { | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(findLoadingSpinner().exists()).toBe(false); | ||||||
|  |       expect(findSkeletonLoader().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('displays the skeleton loader', () => { | ||||||
|     createComponent(resolverSpy); |     createComponent(resolverSpy); | ||||||
| 
 | 
 | ||||||
|     expect(findSkeletonLoader().exists()).toBe(true); |     expect(findSkeletonLoader().exists()).toBe(true); | ||||||
|  | @ -91,7 +114,7 @@ describe('Jobs app', () => { | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('does not display main loading state again after fetchMore', async () => { |   it('does not display skeleton loader again after fetchMore', async () => { | ||||||
|     createComponent(resolverSpy); |     createComponent(resolverSpy); | ||||||
| 
 | 
 | ||||||
|     expect(findSkeletonLoader().exists()).toBe(true); |     expect(findSkeletonLoader().exists()).toBe(true); | ||||||
|  |  | ||||||
|  | @ -66,7 +66,6 @@ describe('graph component', () => { | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
|     wrapper.destroy(); |     wrapper.destroy(); | ||||||
|     wrapper = null; |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('with data', () => { |   describe('with data', () => { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { GlButton, GlLoadingIcon } from '@gitlab/ui'; | import { GlButton, GlLoadingIcon, GlPopover } from '@gitlab/ui'; | ||||||
| import { mount } from '@vue/test-utils'; | import { mountExtended } from 'helpers/vue_test_utils_helper'; | ||||||
| import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; | import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; | ||||||
| import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants'; | import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants'; | ||||||
| import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; | import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; | ||||||
|  | @ -9,15 +9,20 @@ import mockPipeline from './linked_pipelines_mock_data'; | ||||||
| describe('Linked pipeline', () => { | describe('Linked pipeline', () => { | ||||||
|   let wrapper; |   let wrapper; | ||||||
| 
 | 
 | ||||||
|  |   const defaultProps = { | ||||||
|  |     pipeline: mockPipeline, | ||||||
|  |     columnTitle: 'Downstream', | ||||||
|  |     type: DOWNSTREAM, | ||||||
|  |     expanded: false, | ||||||
|  |     isLoading: false, | ||||||
|  |     isMultiProjectVizAvailable: true, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const downstreamProps = { |   const downstreamProps = { | ||||||
|     pipeline: { |     pipeline: { | ||||||
|       ...mockPipeline, |       ...mockPipeline, | ||||||
|       multiproject: false, |       multiproject: false, | ||||||
|     }, |     }, | ||||||
|     columnTitle: 'Downstream', |  | ||||||
|     type: DOWNSTREAM, |  | ||||||
|     expanded: false, |  | ||||||
|     isLoading: false, |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const upstreamProps = { |   const upstreamProps = { | ||||||
|  | @ -27,21 +32,29 @@ describe('Linked pipeline', () => { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const findButton = () => wrapper.find(GlButton); |   const findButton = () => wrapper.find(GlButton); | ||||||
|   const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]'); |   const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title'); | ||||||
|   const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); |   const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label'); | ||||||
|   const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); |   const findLinkedPipeline = () => wrapper.findByTestId('linkedPipeline'); | ||||||
|  |   const findLinkedPipelineBody = () => wrapper.findByTestId('linkedPipelineBody'); | ||||||
|   const findLoadingIcon = () => wrapper.find(GlLoadingIcon); |   const findLoadingIcon = () => wrapper.find(GlLoadingIcon); | ||||||
|   const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]'); |   const findPipelineLink = () => wrapper.findByTestId('pipelineLink'); | ||||||
|   const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]'); |   const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button'); | ||||||
|  |   const findPopover = () => wrapper.find(GlPopover); | ||||||
| 
 | 
 | ||||||
|   const createWrapper = (propsData, data = []) => { |   const createWrapper = (propsData, data = []) => { | ||||||
|     wrapper = mount(LinkedPipelineComponent, { |     wrapper = mountExtended(LinkedPipelineComponent, { | ||||||
|       propsData, |       propsData: { | ||||||
|  |         ...defaultProps, | ||||||
|  |         ...propsData, | ||||||
|  |       }, | ||||||
|       data() { |       data() { | ||||||
|         return { |         return { | ||||||
|           ...data, |           ...data, | ||||||
|         }; |         }; | ||||||
|       }, |       }, | ||||||
|  |       provide: { | ||||||
|  |         multiProjectHelpPath: '/ci/pipelines/multi-project-pipelines', | ||||||
|  |       }, | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -92,7 +105,7 @@ describe('Linked pipeline', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should render the tooltip text as the title attribute', () => { |     it('should render the tooltip text as the title attribute', () => { | ||||||
|       const titleAttr = findLinkedPipeline().attributes('title'); |       const titleAttr = findLinkedPipelineBody().attributes('title'); | ||||||
| 
 | 
 | ||||||
|       expect(titleAttr).toContain(mockPipeline.project.name); |       expect(titleAttr).toContain(mockPipeline.project.name); | ||||||
|       expect(titleAttr).toContain(mockPipeline.status.label); |       expect(titleAttr).toContain(mockPipeline.status.label); | ||||||
|  | @ -168,10 +181,6 @@ describe('Linked pipeline', () => { | ||||||
| 
 | 
 | ||||||
|   describe('when isLoading is true', () => { |   describe('when isLoading is true', () => { | ||||||
|     const props = { |     const props = { | ||||||
|       pipeline: mockPipeline, |  | ||||||
|       columnTitle: 'Downstream', |  | ||||||
|       type: DOWNSTREAM, |  | ||||||
|       expanded: false, |  | ||||||
|       isLoading: true, |       isLoading: true, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -184,19 +193,43 @@ describe('Linked pipeline', () => { | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('on click/hover', () => { |   describe('when the user does not have access to the multi-project pipeline viz feature', () => { | ||||||
|     const props = { |  | ||||||
|       pipeline: mockPipeline, |  | ||||||
|       columnTitle: 'Downstream', |  | ||||||
|       type: DOWNSTREAM, |  | ||||||
|       expanded: false, |  | ||||||
|       isLoading: false, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|  |       const props = { isMultiProjectVizAvailable: false }; | ||||||
|       createWrapper(props); |       createWrapper(props); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     it('the multi-project expand button is disabled', () => { | ||||||
|  |       expect(findExpandButton().props('disabled')).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('it adds the popover text inside the DOM', () => { | ||||||
|  |       expect(findPopover().exists()).toBe(true); | ||||||
|  |       expect(findPopover().text()).toContain( | ||||||
|  |         'Gitlab Premium users have access to the multi-project pipeline graph to improve the visualization of these pipelines.', | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('when the user has access to the multi-project pipeline viz feature', () => { | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createWrapper(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('the multi-project expand button is enabled', () => { | ||||||
|  |       expect(findExpandButton().props('disabled')).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('does not add the popover text inside the DOM', () => { | ||||||
|  |       expect(findPopover().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('on click/hover', () => { | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createWrapper(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     it('emits `pipelineClicked` event', () => { |     it('emits `pipelineClicked` event', () => { | ||||||
|       jest.spyOn(wrapper.vm, '$emit'); |       jest.spyOn(wrapper.vm, '$emit'); | ||||||
|       findButton().trigger('click'); |       findButton().trigger('click'); | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse); | ||||||
| describe('Linked Pipelines Column', () => { | describe('Linked Pipelines Column', () => { | ||||||
|   const defaultProps = { |   const defaultProps = { | ||||||
|     columnTitle: 'Downstream', |     columnTitle: 'Downstream', | ||||||
|  |     isMultiProjectVizAvailable: true, | ||||||
|     linkedPipelines: processedPipeline.downstream, |     linkedPipelines: processedPipeline.downstream, | ||||||
|     showLinks: false, |     showLinks: false, | ||||||
|     type: DOWNSTREAM, |     type: DOWNSTREAM, | ||||||
|  | @ -51,6 +52,9 @@ describe('Linked Pipelines Column', () => { | ||||||
|         ...defaultProps, |         ...defaultProps, | ||||||
|         ...props, |         ...props, | ||||||
|       }, |       }, | ||||||
|  |       provide: { | ||||||
|  |         multiProjectHelpPath: 'ci/pipelines/multi-project-pipeline', | ||||||
|  |       }, | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -67,7 +71,6 @@ describe('Linked Pipelines Column', () => { | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
|     wrapper.destroy(); |     wrapper.destroy(); | ||||||
|     wrapper = null; |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('it renders correctly', () => { |   describe('it renders correctly', () => { | ||||||
|  |  | ||||||
|  | @ -13,6 +13,15 @@ export const mockPipelineResponse = { | ||||||
|         usesNeeds: true, |         usesNeeds: true, | ||||||
|         downstream: null, |         downstream: null, | ||||||
|         upstream: null, |         upstream: null, | ||||||
|  |         user: { | ||||||
|  |           __typename: 'UserCore', | ||||||
|  |           id: 'gid://gitlab/User/1', | ||||||
|  |           namespace: { | ||||||
|  |             __typename: 'Namespace', | ||||||
|  |             id: 'gid://gitlab/Namespaces::UserNamespace/1', | ||||||
|  |             crossProjectPipelineAvailable: true, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|         userPermissions: { |         userPermissions: { | ||||||
|           __typename: 'PipelinePermissions', |           __typename: 'PipelinePermissions', | ||||||
|           updatePipeline: true, |           updatePipeline: true, | ||||||
|  | @ -780,6 +789,15 @@ export const wrappedPipelineReturn = { | ||||||
|         id: 'gid://gitlab/Ci::Pipeline/175', |         id: 'gid://gitlab/Ci::Pipeline/175', | ||||||
|         iid: '38', |         iid: '38', | ||||||
|         complete: true, |         complete: true, | ||||||
|  |         user: { | ||||||
|  |           __typename: 'UserCore', | ||||||
|  |           id: 'gid://gitlab/User/1', | ||||||
|  |           namespace: { | ||||||
|  |             __typename: 'Namespace', | ||||||
|  |             id: 'gid://gitlab/Namespaces::UserNamespace/1', | ||||||
|  |             crossProjectPipelineAvailable: true, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|         usesNeeds: true, |         usesNeeds: true, | ||||||
|         userPermissions: { |         userPermissions: { | ||||||
|           __typename: 'PipelinePermissions', |           __typename: 'PipelinePermissions', | ||||||
|  |  | ||||||
|  | @ -30,14 +30,11 @@ describe('Pipeline Url Component', () => { | ||||||
| 
 | 
 | ||||||
|   const defaultProps = mockPipeline(projectPath); |   const defaultProps = mockPipeline(projectPath); | ||||||
| 
 | 
 | ||||||
|   const createComponent = (props, rearrangePipelinesTable = false) => { |   const createComponent = (props) => { | ||||||
|     wrapper = shallowMountExtended(PipelineUrlComponent, { |     wrapper = shallowMountExtended(PipelineUrlComponent, { | ||||||
|       propsData: { ...defaultProps, ...props }, |       propsData: { ...defaultProps, ...props }, | ||||||
|       provide: { |       provide: { | ||||||
|         targetProjectFullPath: projectPath, |         targetProjectFullPath: projectPath, | ||||||
|         glFeatures: { |  | ||||||
|           rearrangePipelinesTable, |  | ||||||
|         }, |  | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|  | @ -47,7 +44,6 @@ describe('Pipeline Url Component', () => { | ||||||
|     wrapper = null; |     wrapper = null; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('with the rearrangePipelinesTable feature flag turned off', () => { |  | ||||||
|   it('should render pipeline url table cell', () => { |   it('should render pipeline url table cell', () => { | ||||||
|     createComponent(); |     createComponent(); | ||||||
| 
 | 
 | ||||||
|  | @ -194,15 +190,6 @@ describe('Pipeline Url Component', () => { | ||||||
|     expect(findTrainTag().exists()).toBe(false); |     expect(findTrainTag().exists()).toBe(false); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|     it('should not render the commit wrapper and commit-short-sha', () => { |  | ||||||
|       createComponent(); |  | ||||||
| 
 |  | ||||||
|       expect(findCommitTitleContainer().exists()).toBe(false); |  | ||||||
|       expect(findCommitShortSha().exists()).toBe(false); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('with the rearrangePipelinesTable feature flag turned on', () => { |  | ||||||
|   it('should render the commit title, commit reference and commit-short-sha', () => { |   it('should render the commit title, commit reference and commit-short-sha', () => { | ||||||
|     createComponent({}, true); |     createComponent({}, true); | ||||||
| 
 | 
 | ||||||
|  | @ -224,13 +211,9 @@ describe('Pipeline Url Component', () => { | ||||||
|     ${mockPipelineTag()}    | ${'Tag'} |     ${mockPipelineTag()}    | ${'Tag'} | ||||||
|     ${mockPipelineBranch()} | ${'Branch'} |     ${mockPipelineBranch()} | ${'Branch'} | ||||||
|     ${mockPipeline()}       | ${'Merge Request'} |     ${mockPipeline()}       | ${'Merge Request'} | ||||||
|     `(
 |   `('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => {
 | ||||||
|       'should render tooltip $expectedTitle for commit icon type', |  | ||||||
|       ({ pipeline, expectedTitle }) => { |  | ||||||
|     createComponent(pipeline, true); |     createComponent(pipeline, true); | ||||||
| 
 | 
 | ||||||
|     expect(findCommitIconType().attributes('title')).toBe(expectedTitle); |     expect(findCommitIconType().attributes('title')).toBe(expectedTitle); | ||||||
|       }, |  | ||||||
|     ); |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -17,7 +17,6 @@ import { | ||||||
| 
 | 
 | ||||||
| import eventHub from '~/pipelines/event_hub'; | import eventHub from '~/pipelines/event_hub'; | ||||||
| import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; | import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; | ||||||
| import CommitComponent from '~/vue_shared/components/commit.vue'; |  | ||||||
| 
 | 
 | ||||||
| jest.mock('~/pipelines/event_hub'); | jest.mock('~/pipelines/event_hub'); | ||||||
| 
 | 
 | ||||||
|  | @ -37,18 +36,13 @@ describe('Pipelines Table', () => { | ||||||
|     return pipelines.find((p) => p.user !== null && p.commit !== null); |     return pipelines.find((p) => p.user !== null && p.commit !== null); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const createComponent = (props = {}, rearrangePipelinesTable = false) => { |   const createComponent = (props = {}) => { | ||||||
|     wrapper = extendedWrapper( |     wrapper = extendedWrapper( | ||||||
|       mount(PipelinesTable, { |       mount(PipelinesTable, { | ||||||
|         propsData: { |         propsData: { | ||||||
|           ...defaultProps, |           ...defaultProps, | ||||||
|           ...props, |           ...props, | ||||||
|         }, |         }, | ||||||
|         provide: { |  | ||||||
|           glFeatures: { |  | ||||||
|             rearrangePipelinesTable, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }), |       }), | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  | @ -57,7 +51,6 @@ describe('Pipelines Table', () => { | ||||||
|   const findStatusBadge = () => wrapper.findComponent(CiBadge); |   const findStatusBadge = () => wrapper.findComponent(CiBadge); | ||||||
|   const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); |   const findPipelineInfo = () => wrapper.findComponent(PipelineUrl); | ||||||
|   const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); |   const findTriggerer = () => wrapper.findComponent(PipelineTriggerer); | ||||||
|   const findCommit = () => wrapper.findComponent(CommitComponent); |  | ||||||
|   const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); |   const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); | ||||||
|   const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago); |   const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago); | ||||||
|   const findActions = () => wrapper.findComponent(PipelineOperations); |   const findActions = () => wrapper.findComponent(PipelineOperations); | ||||||
|  | @ -65,10 +58,7 @@ describe('Pipelines Table', () => { | ||||||
|   const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); |   const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row'); | ||||||
|   const findStatusTh = () => wrapper.findByTestId('status-th'); |   const findStatusTh = () => wrapper.findByTestId('status-th'); | ||||||
|   const findPipelineTh = () => wrapper.findByTestId('pipeline-th'); |   const findPipelineTh = () => wrapper.findByTestId('pipeline-th'); | ||||||
|   const findTriggererTh = () => wrapper.findByTestId('triggerer-th'); |  | ||||||
|   const findCommitTh = () => wrapper.findByTestId('commit-th'); |  | ||||||
|   const findStagesTh = () => wrapper.findByTestId('stages-th'); |   const findStagesTh = () => wrapper.findByTestId('stages-th'); | ||||||
|   const findTimeAgoTh = () => wrapper.findByTestId('timeago-th'); |  | ||||||
|   const findActionsTh = () => wrapper.findByTestId('actions-th'); |   const findActionsTh = () => wrapper.findByTestId('actions-th'); | ||||||
|   const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); |   const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button'); | ||||||
|   const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); |   const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button'); | ||||||
|  | @ -82,7 +72,7 @@ describe('Pipelines Table', () => { | ||||||
|     wrapper = null; |     wrapper = null; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('Pipelines Table with rearrangePipelinesTable feature flag turned off', () => { |   describe('Pipelines Table', () => { | ||||||
|     beforeEach(() => { |     beforeEach(() => { | ||||||
|       createComponent({ pipelines: [pipeline], viewType: 'root' }); |       createComponent({ pipelines: [pipeline], viewType: 'root' }); | ||||||
|     }); |     }); | ||||||
|  | @ -93,11 +83,8 @@ describe('Pipelines Table', () => { | ||||||
| 
 | 
 | ||||||
|     it('should render table head with correct columns', () => { |     it('should render table head with correct columns', () => { | ||||||
|       expect(findStatusTh().text()).toBe('Status'); |       expect(findStatusTh().text()).toBe('Status'); | ||||||
|       expect(findPipelineTh().text()).toBe('Pipeline ID'); |       expect(findPipelineTh().text()).toBe('Pipeline'); | ||||||
|       expect(findTriggererTh().text()).toBe('Triggerer'); |  | ||||||
|       expect(findCommitTh().text()).toBe('Commit'); |  | ||||||
|       expect(findStagesTh().text()).toBe('Stages'); |       expect(findStagesTh().text()).toBe('Stages'); | ||||||
|       expect(findTimeAgoTh().text()).toBe('Duration'); |  | ||||||
|       expect(findActionsTh().text()).toBe('Actions'); |       expect(findActionsTh().text()).toBe('Actions'); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -125,27 +112,6 @@ describe('Pipelines Table', () => { | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('triggerer cell', () => { |  | ||||||
|       it('should render the pipeline triggerer', () => { |  | ||||||
|         expect(findTriggerer().exists()).toBe(true); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     describe('commit cell', () => { |  | ||||||
|       it('should render commit information', () => { |  | ||||||
|         expect(findCommit().exists()).toBe(true); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('should display and link to commit', () => { |  | ||||||
|         expect(findCommit().text()).toContain(pipeline.commit.short_id); |  | ||||||
|         expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('should display the commit author', () => { |  | ||||||
|         expect(findCommit().props('author')).toEqual(pipeline.commit.author); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     describe('stages cell', () => { |     describe('stages cell', () => { | ||||||
|       it('should render a pipeline mini graph', () => { |       it('should render a pipeline mini graph', () => { | ||||||
|         expect(findPipelineMiniGraph().exists()).toBe(true); |         expect(findPipelineMiniGraph().exists()).toBe(true); | ||||||
|  | @ -163,7 +129,7 @@ describe('Pipelines Table', () => { | ||||||
|           pipeline = createMockPipeline(); |           pipeline = createMockPipeline(); | ||||||
|           pipeline.details.stages = null; |           pipeline.details.stages = null; | ||||||
| 
 | 
 | ||||||
|           createComponent({ pipelines: [pipeline] }, true); |           createComponent({ pipelines: [pipeline] }); | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         it('stages are not rendered', () => { |         it('stages are not rendered', () => { | ||||||
|  | @ -176,7 +142,7 @@ describe('Pipelines Table', () => { | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       it('when update graph dropdown is set, should update graph dropdown', () => { |       it('when update graph dropdown is set, should update graph dropdown', () => { | ||||||
|         createComponent({ pipelines: [pipeline], updateGraphDropdown: true }, true); |         createComponent({ pipelines: [pipeline], updateGraphDropdown: true }); | ||||||
| 
 | 
 | ||||||
|         expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true); |         expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true); | ||||||
|       }); |       }); | ||||||
|  | @ -207,30 +173,11 @@ describe('Pipelines Table', () => { | ||||||
|         expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); |         expect(findCancelBtn().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('Pipelines Table with rearrangePipelinesTable feature flag turned on', () => { |  | ||||||
|     beforeEach(() => { |  | ||||||
|       createComponent({ pipelines: [pipeline], viewType: 'root' }, true); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('should render table head with correct columns', () => { |  | ||||||
|       expect(findStatusTh().text()).toBe('Status'); |  | ||||||
|       expect(findPipelineTh().text()).toBe('Pipeline'); |  | ||||||
|       expect(findStagesTh().text()).toBe('Stages'); |  | ||||||
|       expect(findActionsTh().text()).toBe('Actions'); |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     describe('triggerer cell', () => { |     describe('triggerer cell', () => { | ||||||
|       it('should render the pipeline triggerer', () => { |       it('should render the pipeline triggerer', () => { | ||||||
|         expect(findTriggerer().exists()).toBe(true); |         expect(findTriggerer().exists()).toBe(true); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     describe('commit cell', () => { |  | ||||||
|       it('should not render commit information', () => { |  | ||||||
|         expect(findCommit().exists()).toBe(false); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -34,21 +34,19 @@ exports[`Issue Warning Component when noteable is locked and confidential render | ||||||
| <span> | <span> | ||||||
|   <span> |   <span> | ||||||
|     This issue is  |     This issue is  | ||||||
|     <a |     <gl-link-stub | ||||||
|       href="" |       href="" | ||||||
|       rel="noopener noreferrer" |  | ||||||
|       target="_blank" |       target="_blank" | ||||||
|     > |     > | ||||||
|       confidential |       confidential | ||||||
|     </a> |     </gl-link-stub> | ||||||
|      and  |      and  | ||||||
|     <a |     <gl-link-stub | ||||||
|       href="" |       href="" | ||||||
|       rel="noopener noreferrer" |  | ||||||
|       target="_blank" |       target="_blank" | ||||||
|     > |     > | ||||||
|       locked |       locked | ||||||
|     </a> |     </gl-link-stub> | ||||||
|     . |     . | ||||||
|   </span> |   </span> | ||||||
|    |    | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { GlIcon } from '@gitlab/ui'; | import { GlIcon, GlSprintf } from '@gitlab/ui'; | ||||||
| import { shallowMount } from '@vue/test-utils'; | import { shallowMount } from '@vue/test-utils'; | ||||||
| import { nextTick } from 'vue'; | import { nextTick } from 'vue'; | ||||||
| import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; | import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; | ||||||
|  | @ -16,6 +16,9 @@ describe('Issue Warning Component', () => { | ||||||
|       propsData: { |       propsData: { | ||||||
|         ...props, |         ...props, | ||||||
|       }, |       }, | ||||||
|  |       stubs: { | ||||||
|  |         GlSprintf, | ||||||
|  |       }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|   afterEach(() => { |   afterEach(() => { | ||||||
|  |  | ||||||
|  | @ -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 |       it 'sets the values defined by the declarative enum' do | ||||||
|         set_declarative_enum |         set_declarative_enum | ||||||
| 
 | 
 | ||||||
|         expect(enum_type.values.keys).to eq(['FOO']) |         expect(enum_type.values.keys).to contain_exactly('FOO') | ||||||
|         expect(enum_type.values.values.map(&:description)).to eq(['description of foo']) |         expect(enum_type.values.values.map(&:description)).to contain_exactly('description of foo') | ||||||
|         expect(enum_type.values.values.map(&:value)).to eq([0]) |         expect(enum_type.values.values.map(&:value)).to contain_exactly('foo') | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -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') |       expect(subject).to eq('Blob') | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do |     context 'with a 307 redirect' do | ||||||
|  |       let(:redirect_location) { 'http://redirected' } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|         stub_request(method, url) |         stub_request(method, url) | ||||||
|           .with(headers: blob_headers) |           .with(headers: blob_headers) | ||||||
|         .to_return(status: 307, body: '', headers: { Location: 'http://redirected' }) |           .to_return(status: 307, body: '', headers: { Location: redirect_location }) | ||||||
|  | 
 | ||||||
|         # We should probably use hash_excluding here, but that requires an update to WebMock: |         # We should probably use hash_excluding here, but that requires an update to WebMock: | ||||||
|         # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb |         # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb | ||||||
|       stub_request(:get, "http://redirected/") |         stub_request(:get, redirect_location) | ||||||
|           .with(headers: redirect_header) do |request| |           .with(headers: redirect_header) do |request| | ||||||
|             !request.headers.include?('Authorization') |             !request.headers.include?('Authorization') | ||||||
|           end |           end | ||||||
|           .to_return(status: 200, body: "Successfully redirected") |           .to_return(status: 200, body: "Successfully redirected") | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       shared_examples 'handling redirects' do | ||||||
|  |         it 'follows the redirect' do | ||||||
|  |           expect(Faraday::Utils).not_to receive(:escape).with('signature=') | ||||||
|  |           expect_new_faraday | ||||||
|  |           expect(subject).to eq('Successfully redirected') | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it_behaves_like 'handling redirects' | ||||||
|  | 
 | ||||||
|  |       context 'with a redirect location with params ending with =' do | ||||||
|  |         let(:redirect_location) { 'http://redirect?foo=bar&test=signature=' } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'handling redirects' | ||||||
|  | 
 | ||||||
|  |         context 'with container_registry_follow_redirects_middleware disabled' do | ||||||
|  |           before do | ||||||
|  |             stub_feature_flags(container_registry_follow_redirects_middleware: false) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'follows the redirect' do | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('test').and_call_original | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original | ||||||
| 
 | 
 | ||||||
|             expect_new_faraday(times: 2) |             expect_new_faraday(times: 2) | ||||||
| 
 |  | ||||||
|             expect(subject).to eq('Successfully redirected') |             expect(subject).to eq('Successfully redirected') | ||||||
|           end |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with a redirect location with params ending with %3D' do | ||||||
|  |         let(:redirect_location) { 'http://redirect?foo=bar&test=signature%3D' } | ||||||
|  | 
 | ||||||
|  |         it_behaves_like 'handling redirects' | ||||||
|  | 
 | ||||||
|  |         context 'with container_registry_follow_redirects_middleware disabled' do | ||||||
|  |           before do | ||||||
|  |             stub_feature_flags(container_registry_follow_redirects_middleware: false) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'follows the redirect' do | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('foo').and_call_original | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('bar').and_call_original | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('test').and_call_original | ||||||
|  |             expect(Faraday::Utils).to receive(:escape).with('signature=').and_call_original | ||||||
|  | 
 | ||||||
|  |             expect_new_faraday(times: 2) | ||||||
|  |             expect(subject).to eq('Successfully redirected') | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'handling timeouts' |     it_behaves_like 'handling timeouts' | ||||||
|  | 
 | ||||||
|  |     # TODO Remove this context along with the | ||||||
|  |     # container_registry_follow_redirects_middleware feature flag | ||||||
|  |     # See https://gitlab.com/gitlab-org/gitlab/-/issues/353291 | ||||||
|  |     context 'faraday blob' do | ||||||
|  |       subject { client.send(:faraday_blob) } | ||||||
|  | 
 | ||||||
|  |       it 'has a follow redirects middleware' do | ||||||
|  |         expect(subject.builder.handlers).to include(::FaradayMiddleware::FollowRedirects) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       context 'with container_registry_follow_redirects_middleware is disabled' do | ||||||
|  |         before do | ||||||
|  |           stub_feature_flags(container_registry_follow_redirects_middleware: false) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it 'has  not a follow redirects middleware' do | ||||||
|  |           expect(subject.builder.handlers).not_to include(::FaradayMiddleware::FollowRedirects) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#upload_blob' do |   describe '#upload_blob' do | ||||||
|  |  | ||||||
|  | @ -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 | RSpec.describe IssueLinks::DestroyService do | ||||||
|   describe '#execute' do |   describe '#execute' do | ||||||
|     let(:project) { create(:project_empty_repo) } |     let_it_be(:project) { create(:project_empty_repo, :private) } | ||||||
|     let(:user) { create(:user) } |     let_it_be(:user) { create(:user) } | ||||||
|  |     let_it_be(:issue_a) { create(:issue, project: project) } | ||||||
|  |     let_it_be(:issue_b) { create(:issue, project: project) } | ||||||
| 
 | 
 | ||||||
|     subject { described_class.new(issue_link, user).execute } |     let!(:issuable_link) { create(:issue_link, source: issue_a, target: issue_b) } | ||||||
| 
 | 
 | ||||||
|     context 'when successfully removes an issue link' do |     subject { described_class.new(issuable_link, user).execute } | ||||||
|       let(:issue_a) { create(:issue, project: project) } |  | ||||||
|       let(:issue_b) { create(:issue, project: project) } |  | ||||||
| 
 | 
 | ||||||
|       let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) } |     it_behaves_like 'a destroyable issuable link' | ||||||
| 
 | 
 | ||||||
|  |     context 'when target is an incident' do | ||||||
|       before do |       before do | ||||||
|         project.add_reporter(user) |         project.add_reporter(user) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'removes related issue' do |  | ||||||
|         expect { subject }.to change(IssueLink, :count).from(1).to(0) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'creates notes' do |  | ||||||
|         # Two-way notes creation |  | ||||||
|         expect(SystemNoteService).to receive(:unrelate_issue) |  | ||||||
|                                        .with(issue_link.source, issue_link.target, user) |  | ||||||
|         expect(SystemNoteService).to receive(:unrelate_issue) |  | ||||||
|                                        .with(issue_link.target, issue_link.source, user) |  | ||||||
| 
 |  | ||||||
|         subject |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns success message' do |  | ||||||
|         is_expected.to eq(message: 'Relation was removed', status: :success) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       context 'target is an incident' do |  | ||||||
|       let(:issue_b) { create(:incident, project: project) } |       let(:issue_b) { create(:incident, project: project) } | ||||||
| 
 | 
 | ||||||
|       it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do |       it_behaves_like 'an incident management tracked event', :incident_management_incident_unrelate do | ||||||
|  | @ -45,25 +27,4 @@ RSpec.describe IssueLinks::DestroyService do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 |  | ||||||
|     context 'when failing to remove an issue link' do |  | ||||||
|       let(:unauthorized_project) { create(:project) } |  | ||||||
|       let(:issue_a) { create(:issue, project: project) } |  | ||||||
|       let(:issue_b) { create(:issue, project: unauthorized_project) } |  | ||||||
| 
 |  | ||||||
|       let!(:issue_link) { create(:issue_link, source: issue_a, target: issue_b) } |  | ||||||
| 
 |  | ||||||
|       it 'does not remove relation' do |  | ||||||
|         expect { subject }.not_to change(IssueLink, :count).from(1) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'does not create notes' do |  | ||||||
|         expect(SystemNoteService).not_to receive(:unrelate_issue) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns error message' do |  | ||||||
|         is_expected.to eq(message: 'No Issue Link found', status: :error, http_status: 404) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -117,7 +117,7 @@ RSpec.describe SystemNoteService do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '.unrelate_issue' do |   describe '.unrelate_issuable' do | ||||||
|     let(:noteable_ref) { double } |     let(:noteable_ref) { double } | ||||||
|     let(:noteable) { double } |     let(:noteable) { double } | ||||||
| 
 | 
 | ||||||
|  | @ -127,10 +127,10 @@ RSpec.describe SystemNoteService do | ||||||
| 
 | 
 | ||||||
|     it 'calls IssuableService' do |     it 'calls IssuableService' do | ||||||
|       expect_next_instance_of(::SystemNotes::IssuablesService) do |service| |       expect_next_instance_of(::SystemNotes::IssuablesService) do |service| | ||||||
|         expect(service).to receive(:unrelate_issue).with(noteable_ref) |         expect(service).to receive(:unrelate_issuable).with(noteable_ref) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       described_class.unrelate_issue(noteable, noteable_ref, double) |       described_class.unrelate_issuable(noteable, noteable_ref, double) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,10 +30,10 @@ RSpec.describe ::SystemNotes::IssuablesService do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#unrelate_issue' do |   describe '#unrelate_issuable' do | ||||||
|     let(:noteable_ref) { create(:issue) } |     let(:noteable_ref) { create(:issue) } | ||||||
| 
 | 
 | ||||||
|     subject { service.unrelate_issue(noteable_ref) } |     subject { service.unrelate_issuable(noteable_ref) } | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'a system note' do |     it_behaves_like 'a system note' do | ||||||
|       let(:action) { 'unrelate' } |       let(:action) { 'unrelate' } | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe WebHooks::LogExecutionService do | RSpec.describe WebHooks::LogExecutionService do | ||||||
|   include ExclusiveLeaseHelpers |   include ExclusiveLeaseHelpers | ||||||
|  |   using RSpec::Parameterized::TableSyntax | ||||||
| 
 | 
 | ||||||
|   describe '#execute' do |   describe '#execute' do | ||||||
|     around do |example| |     around do |example| | ||||||
|  | @ -34,11 +35,13 @@ RSpec.describe WebHooks::LogExecutionService do | ||||||
|       expect(WebHookLog.recent.first).to have_attributes(data) |       expect(WebHookLog.recent.first).to have_attributes(data) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'obtaining an exclusive lease' do | ||||||
|  |       let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" } | ||||||
|  | 
 | ||||||
|       it 'updates failure state using a lease that ensures fresh state is written' do |       it 'updates failure state using a lease that ensures fresh state is written' do | ||||||
|         service = described_class.new(hook: project_hook, log_data: data, response_category: :error) |         service = described_class.new(hook: project_hook, log_data: data, response_category: :error) | ||||||
|         WebHook.find(project_hook.id).update!(backoff_count: 1) |         WebHook.find(project_hook.id).update!(backoff_count: 1) | ||||||
| 
 | 
 | ||||||
|       lease_key = "web_hooks:update_hook_failure_state:#{project_hook.id}" |  | ||||||
|         lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL) |         lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL) | ||||||
| 
 | 
 | ||||||
|         expect(lease).to receive(:try_obtain) |         expect(lease).to receive(:try_obtain) | ||||||
|  | @ -46,6 +49,35 @@ RSpec.describe WebHooks::LogExecutionService do | ||||||
|         expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2) |         expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       context 'when a lease cannot be obtained' do | ||||||
|  |         where(:response_category, :executable, :needs_updating) do | ||||||
|  |           :ok     | true  | false | ||||||
|  |           :ok     | false | true | ||||||
|  |           :failed | true  | true | ||||||
|  |           :failed | false | false | ||||||
|  |           :error  | true  | true | ||||||
|  |           :error  | false | false | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         with_them do | ||||||
|  |           subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) } | ||||||
|  | 
 | ||||||
|  |           before do | ||||||
|  |             stub_exclusive_lease_taken(lease_key, timeout: described_class::LOCK_TTL) | ||||||
|  |             allow(project_hook).to receive(:executable?).and_return(executable) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'raises an error if the hook needs to be updated' do | ||||||
|  |             if needs_updating | ||||||
|  |               expect { service.execute }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) | ||||||
|  |             else | ||||||
|  |               expect { service.execute }.not_to raise_error | ||||||
|  |             end | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     context 'when response_category is :ok' do |     context 'when response_category is :ok' do | ||||||
|       it 'does not increment the failure count' do |       it 'does not increment the failure count' do | ||||||
|         expect { service.execute }.not_to change(project_hook, :recent_failures) |         expect { service.execute }.not_to change(project_hook, :recent_failures) | ||||||
|  |  | ||||||
|  | @ -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