summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines/components/graph
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue17
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue7
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