diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
7 files changed, 157 insertions, 11 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js index 85ca52f633e..e650a48bc2a 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -10,6 +10,8 @@ export const ONE_COL_WIDTH = 180; export const STAGE_VIEW = 'stage'; export const LAYER_VIEW = 'layer'; + +export const SKIP_RETRY_MODAL_KEY = 'skip_retry_modal'; export const VIEW_TYPE_KEY = 'pipeline_graph_view_type'; export const SINGLE_JOB = 'single_job'; @@ -20,3 +22,5 @@ export const BRIDGE_KIND = 'BRIDGE'; export const ACTION_FAILURE = 'action_failure'; export const IID_FAILURE = 'missing_iid'; + +export const RETRY_ACTION_TITLE = 'Retry'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 1a05710a13e..49df71beeec 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -2,7 +2,10 @@ import { reportToSentry } from '../../utils'; import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; import LinksLayer from '../graph_shared/links_layer.vue'; -import { generateColumnsFromLayersListMemoized } from '../parsing_utils'; +import { + generateColumnsFromLayersListMemoized, + keepLatestDownstreamPipelines, +} from '../parsing_utils'; import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; @@ -44,6 +47,11 @@ export default { required: false, default: () => ({}), }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, type: { type: String, required: false, @@ -76,7 +84,9 @@ export default { return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`; }, downstreamPipelines() { - return this.hasDownstreamPipelines ? this.pipeline.downstream : []; + return this.hasDownstreamPipelines + ? keepLatestDownstreamPipelines(this.pipeline.downstream) + : []; }, layout() { return this.isStageView @@ -181,9 +191,11 @@ export default { :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" :show-links="showJobLinks" + :skip-retry-modal="skipRetryModal" :type="$options.pipelineTypeConstants.UPSTREAM" :view-type="viewType" @error="onError" + @setSkipRetryModal="$emit('setSkipRetryModal')" /> </template> <template #main> @@ -210,11 +222,13 @@ export default { :highlighted-jobs="highlightedJobs" :is-stage-view="isStageView" :job-hovered="hoveredJobName" + :skip-retry-modal="skipRetryModal" :source-job-hovered="hoveredSourceJobName" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipeline.id" :user-permissions="pipeline.userPermissions" @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @setSkipRetryModal="$emit('setSkipRetryModal')" @jobHover="setJob" @updateMeasurements="getMeasurements" /> @@ -228,12 +242,15 @@ export default { :config-paths="configPaths" :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" + :skip-retry-modal="skipRetryModal" :show-links="showJobLinks" :type="$options.pipelineTypeConstants.DOWNSTREAM" :view-type="viewType" + data-testid="downstream-pipelines" @downstreamHovered="setSourceJob" @pipelineExpandToggle="togglePipelineExpanded" @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @setSkipRetryModal="$emit('setSkipRetryModal')" @scrollContainer="slidePipelineContainer" @error="onError" /> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 4d7596e6e16..8f76d7535f1 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -8,7 +8,14 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql'; import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql'; import { reportToSentry, reportMessageToSentry } from '../../utils'; -import { ACTION_FAILURE, IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants'; +import { + ACTION_FAILURE, + IID_FAILURE, + LAYER_VIEW, + SKIP_RETRY_MODAL_KEY, + STAGE_VIEW, + VIEW_TYPE_KEY, +} from './constants'; import PipelineGraph from './graph_component.vue'; import GraphViewSelector from './graph_view_selector.vue'; import { @@ -53,6 +60,7 @@ export default { currentViewType: STAGE_VIEW, canRefetchHeaderPipeline: false, pipeline: null, + skipRetryModal: false, showAlert: false, showLinks: false, }; @@ -206,8 +214,8 @@ export default { if (!this.pipelineIid) { this.reportFailure({ type: IID_FAILURE, skipSentry: true }); } - toggleQueryPollingByVisibility(this.$apollo.queries.pipeline); + this.skipRetryModal = Boolean(JSON.parse(localStorage.getItem(SKIP_RETRY_MODAL_KEY))); }, errorCaptured(err, _vm, info) { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); @@ -259,6 +267,9 @@ export default { updateShowLinksState(val) { this.showLinks = val; }, + setSkipRetryModal() { + this.skipRetryModal = true; + }, updateViewType(type) { this.currentViewType = type; }, @@ -293,10 +304,12 @@ export default { :config-paths="configPaths" :pipeline="pipeline" :computed-pipeline-info="getPipelineInfo()" + :skip-retry-modal="skipRetryModal" :show-links="showLinks" :view-type="graphViewType" @error="reportFailure" @refreshPipelineGraph="refreshPipelineGraph" + @setSkipRetryModal="setSkipRetryModal" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4f2be27486c..992e3d2f552 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -1,13 +1,14 @@ <script> -import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { GlBadge, GlForm, GlFormCheckbox, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import { sprintf, __ } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import ActionComponent from '../jobs_shared/action_component.vue'; import JobNameComponent from '../jobs_shared/job_name_component.vue'; -import { BRIDGE_KIND, SINGLE_JOB } from './constants'; +import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from './constants'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -35,17 +36,32 @@ import { BRIDGE_KIND, SINGLE_JOB } from './constants'; */ export default { + confirmationModalDocLink: helpPagePath('/ci/pipelines/downstream_pipelines'), i18n: { bridgeBadgeText: __('Trigger job'), unauthorizedTooltip: __('You are not authorized to run this manual job'), + confirmationModal: { + title: s__('PipelineGraph|Are you sure you want to retry %{jobName}?'), + description: s__( + 'PipelineGraph|Retrying a trigger job will create a new downstream pipeline.', + ), + linkText: s__('PipelineGraph|What is a downstream pipeline?'), + footer: __("Don't show this again"), + actionPrimary: { text: __('Retry') }, + actionCancel: { text: __('Cancel') }, + }, + runAgainTooltipText: __('Run again'), }, hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, CiIcon, - JobNameComponent, GlBadge, + GlForm, + GlFormCheckbox, GlLink, + GlModal, + JobNameComponent, }, directives: { GlTooltip: GlTooltipDirective, @@ -86,6 +102,11 @@ export default { required: false, default: -1, }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, sourceJobHovered: { type: String, required: false, @@ -102,6 +123,13 @@ export default { default: SINGLE_JOB, }, }, + data() { + return { + currentSkipModalValue: this.skipRetryModal, + showConfirmationModal: false, + shouldTriggerActionClick: false, + }; + }, computed: { boundary() { return this.dropdownLength === 1 ? 'viewport' : 'scrollParent'; @@ -115,6 +143,12 @@ export default { hasDetails() { return this.status.hasDetails; }, + hasRetryAction() { + return Boolean(this.job?.status?.action?.title === RETRY_ACTION_TITLE); + }, + isRetryableBridge() { + return this.isBridge && this.hasRetryAction; + }, isSingleItem() { return this.type === SINGLE_JOB; }, @@ -127,6 +161,11 @@ export default { nameComponent() { return this.hasDetails ? 'gl-link' : 'div'; }, + retryTriggerJobWarningText() { + return sprintf(this.$options.i18n.confirmationModal.title, { + jobName: this.job.name, + }); + }, showStageName() { return Boolean(this.stageName); }, @@ -205,11 +244,34 @@ export default { }, ]; }, + withConfirmationModal() { + return this.isRetryableBridge && !this.skipRetryModal; + }, + jobActionTooltipText() { + const { group } = this.status; + const { title, icon } = this.status.action; + + return icon === 'retry' && group === 'success' + ? this.$options.i18n.runAgainTooltipText + : title; + }, + }, + watch: { + skipRetryModal(val) { + this.currentSkipModalValue = val; + this.shouldTriggerActionClick = false; + }, }, errorCaptured(err, _vm, info) { reportToSentry('job_item', `error: ${err}, info: ${info}`); }, methods: { + handleConfirmationModalPreferences() { + if (this.currentSkipModalValue) { + this.$emit('setSkipRetryModal'); + localStorage.setItem(SKIP_RETRY_MODAL_KEY, String(this.currentSkipModalValue)); + } + }, hideTooltips() { this.$root.$emit(BV_HIDE_TOOLTIP); }, @@ -227,6 +289,15 @@ export default { pipelineActionRequestComplete() { this.$emit('pipelineActionRequestComplete'); }, + executePendingAction() { + this.shouldTriggerActionClick = true; + }, + showActionConfirmationModal() { + this.showConfirmationModal = true; + }, + toggleSkipRetryModalCheckbox() { + this.currentSkipModalValue = !this.currentSkipModalValue; + }, }, }; </script> @@ -272,12 +343,16 @@ export default { <action-component v-if="hasAction" - :tooltip-text="status.action.title" + :tooltip-text="jobActionTooltipText" :link="status.action.path" :action-icon="status.action.icon" class="gl-mr-1" + :should-trigger-click="shouldTriggerActionClick" + :with-confirmation-modal="withConfirmationModal" data-qa-selector="job_action_button" + @actionButtonClicked="handleConfirmationModalPreferences" @pipelineActionRequestComplete="pipelineActionRequestComplete" + @showActionConfirmationModal="showActionConfirmationModal" /> <action-component v-if="hasUnauthorizedManualAction" @@ -287,5 +362,28 @@ export default { :link="`unauthorized-${computedJobId}`" class="gl-mr-1" /> + <gl-modal + v-if="showConfirmationModal" + ref="modal" + v-model="showConfirmationModal" + modal-id="action-confirmation-modal" + :title="retryTriggerJobWarningText" + :action-cancel="$options.i18n.confirmationModal.actionCancel" + :action-primary="$options.i18n.confirmationModal.actionPrimary" + @primary="executePendingAction" + @close="handleConfirmationModalPreferences" + @hide="handleConfirmationModalPreferences" + > + <p class="gl-mb-1">{{ $options.i18n.confirmationModal.description }}</p> + <gl-link :href="$options.confirmationModalDocLink" target="_blank">{{ + $options.i18n.confirmationModal.linkText + }}</gl-link> + <div class="gl-mt-4 gl-display-flex"> + <gl-form> + <gl-form-checkbox class="gl-min-h-0" @input="toggleSkipRetryModalCheckbox" /> + </gl-form> + <p class="gl-m-0">{{ $options.i18n.confirmationModal.footer }}</p> + </div> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index 225706265c3..9b4e5d471d6 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -7,13 +7,13 @@ import { GlTooltip, GlTooltipDirective, } from '@gitlab/ui'; +import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql'; import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; -import { PIPELINE_GRAPHQL_TYPE } from '../../constants'; import { reportToSentry } from '../../utils'; import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants'; @@ -118,7 +118,7 @@ export default { return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row'; }, graphqlPipelineId() { - return convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, this.pipeline.id); + return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id); }, hasUpdatePipelinePermissions() { return Boolean(this.pipeline?.userPermissions?.updatePipeline); diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index b06c2f15042..02e426064c9 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -36,6 +36,11 @@ export default { type: Boolean, required: true, }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, type: { type: String, required: true, @@ -229,8 +234,10 @@ export default { :pipeline="currentPipeline" :computed-pipeline-info="getPipelineLayers(pipeline.id)" :show-links="showLinks" + :skip-retry-modal="skipRetryModal" :is-linked-pipeline="true" :view-type="graphViewType" + @setSkipRetryModal="$emit('setSkipRetryModal')" /> </div> </li> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 4aec28295bd..ffd0fec2ca8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -53,6 +53,11 @@ export default { required: false, default: () => ({}), }, + skipRetryModal: { + type: Boolean, + required: false, + default: false, + }, sourceJobHovered: { type: String, required: false, @@ -164,6 +169,7 @@ export default { v-if="singleJobExists(group)" :job="group.jobs[0]" :job-hovered="jobHovered" + :skip-retry-modal="skipRetryModal" :source-job-hovered="sourceJobHovered" :pipeline-expanded="pipelineExpanded" :pipeline-id="pipelineId" @@ -174,6 +180,7 @@ export default { 'gl-transition-duration-slow gl-transition-timing-function-ease', ]" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" + @setSkipRetryModal="$emit('setSkipRetryModal')" /> <div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }"> <job-group-dropdown |