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.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue151
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue10
6 files changed, 186 insertions, 62 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index 0b59612b25c..85ca52f633e 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -15,4 +15,8 @@ export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
export const SINGLE_JOB = 'single_job';
export const JOB_DROPDOWN = 'job_dropdown';
+export const BUILD_KIND = 'BUILD';
+export const BRIDGE_KIND = 'BRIDGE';
+
+export const ACTION_FAILURE = 'action_failure';
export const IID_FAILURE = 'missing_iid';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 015f0519c72..31a34ab4fb5 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -233,6 +233,7 @@ export default {
:view-type="viewType"
@downstreamHovered="setSourceJob"
@pipelineExpandToggle="togglePipelineExpanded"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
@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 534ad25a35d..f822e2c0874 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,7 @@ 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 { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
+import { ACTION_FAILURE, IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue';
import {
@@ -57,13 +57,29 @@ export default {
showLinks: false,
};
},
- errorTexts: {
- [DRAW_FAILURE]: __('An error occurred while drawing job relationship links.'),
- [IID_FAILURE]: __(
- 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
- ),
- [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'),
- [DEFAULT]: __('An unknown error occurred while loading this graph.'),
+ errors: {
+ [ACTION_FAILURE]: {
+ text: __('An error occurred while performing this action.'),
+ variant: 'danger',
+ },
+ [DRAW_FAILURE]: {
+ text: __('An error occurred while drawing job relationship links.'),
+ variant: 'danger',
+ },
+ [IID_FAILURE]: {
+ text: __(
+ 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
+ ),
+ variant: 'info',
+ },
+ [LOAD_FAILURE]: {
+ text: __('Currently unable to fetch data for this pipeline.'),
+ variant: 'danger',
+ },
+ [DEFAULT]: {
+ text: __('An unknown error occurred while loading this graph.'),
+ variant: 'danger',
+ },
},
apollo: {
callouts: {
@@ -154,28 +170,12 @@ export default {
},
computed: {
alert() {
- switch (this.alertType) {
- case DRAW_FAILURE:
- return {
- text: this.$options.errorTexts[DRAW_FAILURE],
- variant: 'danger',
- };
- case IID_FAILURE:
- return {
- text: this.$options.errorTexts[IID_FAILURE],
- variant: 'info',
- };
- case LOAD_FAILURE:
- return {
- text: this.$options.errorTexts[LOAD_FAILURE],
- variant: 'danger',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- };
- }
+ const { errors } = this.$options;
+
+ return {
+ text: errors[this.alertType]?.text ?? errors[DEFAULT].text,
+ variant: errors[this.alertType]?.variant ?? errors[DEFAULT].variant,
+ };
},
configPaths() {
return {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index f69b25dfa7c..362571930d6 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf, __ } from '~/locale';
@@ -7,7 +7,7 @@ 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 { SINGLE_JOB } from './constants';
+import { BRIDGE_KIND, SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -35,11 +35,16 @@ import { SINGLE_JOB } from './constants';
*/
export default {
+ i18n: {
+ bridgeBadgeText: __('Trigger job'),
+ unauthorizedTooltip: __('You are not authorized to run this manual job'),
+ },
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
CiIcon,
JobNameComponent,
+ GlBadge,
GlLink,
},
directives: {
@@ -113,6 +118,12 @@ export default {
isSingleItem() {
return this.type === SINGLE_JOB;
},
+ isBridge() {
+ return this.kind === BRIDGE_KIND;
+ },
+ kind() {
+ return this.job?.kind || '';
+ },
nameComponent() {
return this.hasDetails ? 'gl-link' : 'div';
},
@@ -187,6 +198,7 @@ export default {
[this.$options.hoverClass]:
this.relatedDownstreamHovered || this.relatedDownstreamExpanded,
},
+ { 'gl-rounded-lg': this.isBridge },
this.cssClassJobName,
];
},
@@ -213,9 +225,6 @@ export default {
this.$emit('pipelineActionRequestComplete');
},
},
- i18n: {
- unauthorizedTooltip: __('You are not authorized to run this manual job'),
- },
};
</script>
<template>
@@ -253,6 +262,9 @@ export default {
</div>
</div>
</div>
+ <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
+ {{ $options.i18n.bridgeBadgeText }}
+ </gl-badge>
</component>
<action-component
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index d59802196af..9f76d4cec50 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,10 +1,22 @@
<script>
-import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlBadge,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+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 glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { PIPELINE_GRAPHQL_TYPE } from '../../constants';
import { reportToSentry } from '../../utils';
-import { DOWNSTREAM, UPSTREAM } from './constants';
+import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
export default {
directives: {
@@ -16,7 +28,14 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
+ GlTooltip,
},
+ styles: {
+ actionSizeClasses: ['gl-h-7 gl-w-7'],
+ flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
+ flatRightBorder: ['gl-rounded-bottom-right-none!', 'gl-rounded-top-right-none!'],
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -39,15 +58,44 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ hasActionTooltip: false,
+ isActionLoading: false,
+ };
+ },
computed: {
- buttonBorderClass() {
- return this.isUpstream ? 'gl-border-r-1!' : 'gl-border-l-1!';
+ action() {
+ if (this.glFeatures?.downstreamRetryAction && this.isDownstream) {
+ if (this.isCancelable) {
+ return {
+ icon: 'cancel',
+ method: this.cancelPipeline,
+ ariaLabel: __('Cancel downstream pipeline'),
+ };
+ } else if (this.isRetryable) {
+ return {
+ icon: 'retry',
+ method: this.retryPipeline,
+ ariaLabel: __('Retry downstream pipeline'),
+ };
+ }
+ }
+
+ return {};
+ },
+ buttonBorderClasses() {
+ return this.isUpstream
+ ? ['gl-border-r-0!', ...this.$options.styles.flatRightBorder]
+ : ['gl-border-l-0!', ...this.$options.styles.flatLeftBorder];
},
buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`;
},
- cardSpacingClass() {
- return this.isDownstream ? 'gl-pr-0' : '';
+ cardClasses() {
+ return this.isDownstream
+ ? this.$options.styles.flatRightBorder
+ : this.$options.styles.flatLeftBorder;
},
expandedIcon() {
if (this.isUpstream) {
@@ -64,9 +112,21 @@ export default {
flexDirection() {
return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
},
+ graphqlPipelineId() {
+ return convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, this.pipeline.id);
+ },
+ hasUpdatePipelinePermissions() {
+ return Boolean(this.pipeline?.userPermissions?.updatePipeline);
+ },
+ isCancelable() {
+ return Boolean(this.pipeline?.cancelable && this.hasUpdatePipelinePermissions);
+ },
isDownstream() {
return this.type === DOWNSTREAM;
},
+ isRetryable() {
+ return Boolean(this.pipeline?.retryable && this.hasUpdatePipelinePermissions);
+ },
isSameProject() {
return !this.pipeline.multiproject;
},
@@ -93,13 +153,19 @@ export default {
projectName() {
return this.pipeline.project.name;
},
+ showAction() {
+ return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
+ },
+ showCardTooltip() {
+ return !this.hasActionTooltip;
+ },
sourceJobName() {
return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
},
- tooltipText() {
+ cardTooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
@@ -108,6 +174,26 @@ export default {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
},
methods: {
+ cancelPipeline() {
+ this.executePipelineAction(CancelPipelineMutation);
+ },
+ async executePipelineAction(mutation) {
+ try {
+ this.isActionLoading = true;
+
+ await this.$apollo.mutate({
+ mutation,
+ variables: {
+ id: this.graphqlPipelineId,
+ },
+ });
+ this.$emit('refreshPipelineGraph');
+ } catch {
+ this.$emit('error', { type: ACTION_FAILURE });
+ } finally {
+ this.isActionLoading = false;
+ }
+ },
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
@@ -122,6 +208,12 @@ export default {
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
},
+ retryPipeline() {
+ this.executePipelineAction(RetryPipelineMutation);
+ },
+ setActionTooltip(flag) {
+ this.hasActionTooltip = flag;
+ },
},
};
</script>
@@ -129,33 +221,48 @@ export default {
<template>
<div
ref="linkedPipeline"
- v-gl-tooltip
- class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
+ class="gl-h-full gl-display-flex!"
:class="flexDirection"
- :title="tooltipText"
data-qa-selector="linked_pipeline_container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
- <div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
- <div class="gl-display-flex gl-pr-3">
- <ci-status
- v-if="!pipelineIsLoading"
- :status="pipelineStatus"
- :size="24"
- css-classes="gl-top-0 gl-pr-2"
- />
+ <gl-tooltip v-if="showCardTooltip" :target="() => $refs.linkedPipeline">
+ {{ cardTooltipText }}
+ </gl-tooltip>
+ <div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
+ <div class="gl-display-flex gl-gap-x-3">
+ <ci-status v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" css-classes="" />
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
- <div class="gl-display-flex gl-flex-direction-column gl-downstream-pipeline-job-width">
+ <div
+ class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
+ >
<span class="gl-text-truncate" data-testid="downstream-title">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
- <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink"
+ <gl-link
+ class="gl-text-blue-500! gl-font-sm"
+ :href="pipeline.path"
+ data-testid="pipelineLink"
>#{{ pipeline.id }}</gl-link
>
</div>
</div>
+ <gl-button
+ v-if="showAction"
+ v-gl-tooltip
+ :title="action.ariaLabel"
+ :loading="isActionLoading"
+ :icon="action.icon"
+ class="gl-rounded-full!"
+ :class="$options.styles.actionSizeClasses"
+ :aria-label="action.ariaLabel"
+ @click="action.method"
+ @mouseover="setActionTooltip(true)"
+ @mouseout="setActionTooltip(false)"
+ />
+ <div v-else :class="$options.styles.actionSizeClasses"></div>
</div>
<div class="gl-pt-2">
<gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
@@ -166,8 +273,8 @@ export default {
<div class="gl-display-flex">
<gl-button
:id="buttonId"
- class="gl-shadow-none! gl-rounded-0!"
- :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`"
+ class="gl-border! gl-shadow-none! gl-rounded-lg!"
+ :class="[`js-pipeline-expand-${pipeline.id}`, buttonBorderClasses]"
:icon="expandedIcon"
:aria-label="__('Expand pipeline')"
data-testid="expand-pipeline-button"
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 3c1208afbf0..b06c2f15042 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -66,14 +66,13 @@ export default {
columnClass() {
const positionValues = {
right: 'gl-ml-6',
- left: 'gl-mr-6',
+ left: 'gl-mx-6',
};
+
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
computedTitleClasses() {
- const positionalClasses = this.isUpstream
- ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
- : [];
+ const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
return [...this.$options.titleClasses, ...positionalClasses];
},
@@ -202,7 +201,7 @@ export default {
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
- class="gl-display-flex gl-mb-4"
+ class="gl-display-flex gl-mb-3"
:class="{ 'gl-flex-direction-row-reverse': isUpstream }"
>
<linked-pipeline
@@ -215,6 +214,7 @@ export default {
@downstreamHovered="onDownstreamHovered"
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
/>
<div
v-if="showContainer(pipeline.id)"