diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/graph')
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)" |