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/accessors.js25
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue270
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue265
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue106
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue70
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue139
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue87
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue118
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js57
15 files changed, 974 insertions, 341 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/accessors.js b/app/assets/javascripts/pipelines/components/graph/accessors.js
new file mode 100644
index 00000000000..6ece855bcd8
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/accessors.js
@@ -0,0 +1,25 @@
+import { get } from 'lodash';
+import { REST, GRAPHQL } from './constants';
+
+const accessors = {
+ [REST]: {
+ detailsPath: 'details_path',
+ groupId: 'id',
+ hasDetails: 'has_details',
+ pipelineStatus: ['details', 'status'],
+ sourceJob: ['source_job', 'name'],
+ },
+ [GRAPHQL]: {
+ detailsPath: 'detailsPath',
+ groupId: 'name',
+ hasDetails: 'hasDetails',
+ pipelineStatus: 'status',
+ sourceJob: ['sourceJob', 'name'],
+ },
+};
+
+const accessValue = (dataMethod, prop, item) => {
+ return get(item, accessors[dataMethod][prop]);
+};
+
+export { accessors, accessValue };
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index a580ee11627..4e9b21a5c55 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -87,10 +87,10 @@ export default {
:title="tooltipText"
:class="cssClass"
:disabled="isDisabled"
- class="js-ci-action ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
+ class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
@click.stop="onClickAction"
>
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
- <gl-icon v-else :name="actionIcon" class="gl-mr-0!" />
+ <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index ba1922b6dae..6f0deccfef6 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -1,3 +1,6 @@
export const DOWNSTREAM = 'downstream';
export const MAIN = 'main';
export const UPSTREAM = 'upstream';
+
+export const REST = 'rest';
+export const GRAPHQL = 'graphql';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 16ce279a591..67b2ed3b596 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,35 +1,23 @@
<script>
-import { escape, capitalize } from 'lodash';
-import { GlLoadingIcon } from '@gitlab/ui';
-import StageColumnComponent from './stage_column_component.vue';
-import GraphWidthMixin from '../../mixins/graph_width_mixin';
+import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
-import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
-import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
+import StageColumnComponent from './stage_column_component.vue';
+import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
export default {
name: 'PipelineGraph',
components: {
- StageColumnComponent,
- GlLoadingIcon,
+ LinkedGraphWrapper,
LinkedPipelinesColumn,
+ StageColumnComponent,
},
- mixins: [GraphWidthMixin, GraphBundleMixin],
props: {
- isLoading: {
- type: Boolean,
- required: true,
- },
- pipeline: {
- type: Object,
- required: true,
- },
isLinkedPipeline: {
type: Boolean,
required: false,
default: false,
},
- mediator: {
+ pipeline: {
type: Object,
required: true,
},
@@ -39,12 +27,13 @@ export default {
default: MAIN,
},
},
- upstream: UPSTREAM,
- downstream: DOWNSTREAM,
+ pipelineTypeConstants: {
+ DOWNSTREAM,
+ UPSTREAM,
+ },
data() {
return {
- downstreamMarginTop: null,
- jobName: null,
+ hoveredJobName: '',
pipelineExpanded: {
jobName: '',
expanded: false,
@@ -52,219 +41,86 @@ export default {
};
},
computed: {
+ downstreamPipelines() {
+ return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
+ },
graph() {
- return this.pipeline.details?.stages;
+ return this.pipeline.stages;
},
- hasUpstream() {
- return (
- this.type !== this.$options.downstream &&
- this.upstreamPipelines &&
- this.pipeline.triggered_by !== null
- );
+ hasDownstreamPipelines() {
+ return Boolean(this.pipeline?.downstream?.length > 0);
},
- upstreamPipelines() {
- return this.pipeline.triggered_by;
+ hasUpstreamPipelines() {
+ return Boolean(this.pipeline?.upstream?.length > 0);
},
- hasDownstream() {
+ // The two show checks prevent upstream / downstream from showing redundant linked columns
+ showDownstreamPipelines() {
return (
- this.type !== this.$options.upstream &&
- this.downstreamPipelines &&
- this.pipeline.triggered.length > 0
+ this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
);
},
- downstreamPipelines() {
- return this.pipeline.triggered;
- },
- expandedUpstream() {
+ showUpstreamPipelines() {
return (
- this.pipeline.triggered_by &&
- Array.isArray(this.pipeline.triggered_by) &&
- this.pipeline.triggered_by.find(el => el.isExpanded)
+ this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
);
},
- expandedDownstream() {
- return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
- },
- pipelineTypeUpstream() {
- return this.type !== this.$options.downstream && this.expandedUpstream;
- },
- pipelineTypeDownstream() {
- return this.type !== this.$options.upstream && this.expandedDownstream;
- },
- pipelineProjectId() {
- return this.pipeline.project.id;
+ upstreamPipelines() {
+ return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
},
methods: {
- capitalizeStageName(name) {
- const escapedName = escape(name);
- return capitalize(escapedName);
- },
- isFirstColumn(index) {
- return index === 0;
- },
- stageConnectorClass(index, stage) {
- let className;
-
- // If it's the first stage column and only has one job
- if (this.isFirstColumn(index) && stage.groups.length === 1) {
- className = 'no-margin';
- } else if (index > 0) {
- // If it is not the first column
- className = 'left-margin';
- }
-
- return className;
- },
- refreshPipelineGraph() {
- this.$emit('refreshPipelineGraph');
- },
- /**
- * CSS class is applied:
- * - if pipeline graph contains only one stage column component
- *
- * @param {number} index
- * @returns {boolean}
- */
- shouldAddRightMargin(index) {
- return !(index === this.graph.length - 1);
- },
- handleClickedDownstream(pipeline, clickedIndex, downstreamNode) {
- /**
- * Calculates the margin top of the clicked downstream pipeline by
- * subtracting the clicked downstream pipelines offsetTop by it's parent's
- * offsetTop and then subtracting 15
- */
- this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
-
- /**
- * If the expanded trigger is defined and the id is different than the
- * pipeline we clicked, then it means we clicked on a sibling downstream link
- * and we want to reset the pipeline store. Triggering the reset without
- * this condition would mean not allowing downstreams of downstreams to expand
- */
- if (this.expandedDownstream?.id !== pipeline.id) {
- this.$emit('onResetDownstream', this.pipeline, pipeline);
- }
-
- this.$emit('onClickDownstreamPipeline', pipeline);
- },
- calculateMarginTop(downstreamNode, pixelDiff) {
- return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
- },
- hasOnlyOneJob(stage) {
- return stage.groups.length === 1;
- },
- hasUpstreamColumn(index) {
- return index === 0 && this.hasUpstream;
- },
setJob(jobName) {
- this.jobName = jobName;
+ this.hoveredJobName = jobName;
},
- setPipelineExpanded(jobName, expanded) {
- if (expanded) {
- this.pipelineExpanded = {
- jobName,
- expanded,
- };
- } else {
- this.pipelineExpanded = {
- expanded,
- jobName: '',
- };
- }
+ togglePipelineExpanded(jobName, expanded) {
+ this.pipelineExpanded = {
+ expanded,
+ jobName: expanded ? jobName : '',
+ };
},
},
};
</script>
<template>
- <div class="build-content middle-block js-pipeline-graph">
+ <div class="js-pipeline-graph">
<div
- class="pipeline-visualization pipeline-graph"
- :class="{ 'pipeline-tab-content': !isLinkedPipeline }"
+ class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
+ :class="{ 'gl-py-5': !isLinkedPipeline }"
>
- <div
- :style="{
- paddingLeft: `${graphLeftPadding}px`,
- paddingRight: `${graphRightPadding}px`,
- }"
- >
- <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
-
- <pipeline-graph
- v-if="pipelineTypeUpstream"
- :type="$options.upstream"
- class="d-inline-block upstream-pipeline"
- :class="`js-upstream-pipeline-${expandedUpstream.id}`"
- :is-loading="false"
- :pipeline="expandedUpstream"
- :is-linked-pipeline="true"
- :mediator="mediator"
- @onClickUpstreamPipeline="clickUpstreamPipeline"
- @refreshPipelineGraph="requestRefreshPipelineGraph"
- />
-
- <linked-pipelines-column
- v-if="hasUpstream"
- :type="$options.upstream"
- :linked-pipelines="upstreamPipelines"
- :column-title="__('Upstream')"
- :project-id="pipelineProjectId"
- @linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
- />
-
- <ul
- v-if="!isLoading"
- :class="{
- 'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
- }"
- class="stage-column-list align-top"
- >
+ <linked-graph-wrapper>
+ <template #upstream>
+ <linked-pipelines-column
+ v-if="showUpstreamPipelines"
+ :linked-pipelines="upstreamPipelines"
+ :column-title="__('Upstream')"
+ :type="$options.pipelineTypeConstants.UPSTREAM"
+ @error="emit('error', errorType)"
+ />
+ </template>
+ <template #main>
<stage-column-component
- v-for="(stage, index) in graph"
+ v-for="stage in graph"
:key="stage.name"
- :class="{
- 'has-upstream gl-ml-11': hasUpstreamColumn(index),
- 'has-only-one-job': hasOnlyOneJob(stage),
- 'gl-mr-26': shouldAddRightMargin(index),
- }"
- :title="capitalizeStageName(stage.name)"
+ :title="stage.name"
:groups="stage.groups"
- :stage-connector-class="stageConnectorClass(index, stage)"
- :is-first-column="isFirstColumn(index)"
- :has-upstream="hasUpstream"
:action="stage.status.action"
- :job-hovered="jobName"
+ :job-hovered="hoveredJobName"
:pipeline-expanded="pipelineExpanded"
- @refreshPipelineGraph="refreshPipelineGraph"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
/>
- </ul>
-
- <linked-pipelines-column
- v-if="hasDownstream"
- :type="$options.downstream"
- :linked-pipelines="downstreamPipelines"
- :column-title="__('Downstream')"
- :project-id="pipelineProjectId"
- @linkedPipelineClick="handleClickedDownstream"
- @downstreamHovered="setJob"
- @pipelineExpandToggle="setPipelineExpanded"
- />
-
- <pipeline-graph
- v-if="pipelineTypeDownstream"
- :type="$options.downstream"
- class="d-inline-block"
- :class="`js-downstream-pipeline-${expandedDownstream.id}`"
- :is-loading="false"
- :pipeline="expandedDownstream"
- :is-linked-pipeline="true"
- :style="{ 'margin-top': downstreamMarginTop }"
- :mediator="mediator"
- @onClickDownstreamPipeline="clickDownstreamPipeline"
- @refreshPipelineGraph="requestRefreshPipelineGraph"
- />
- </div>
+ </template>
+ <template #downstream>
+ <linked-pipelines-column
+ v-if="showDownstreamPipelines"
+ :linked-pipelines="downstreamPipelines"
+ :column-title="__('Downstream')"
+ :type="$options.pipelineTypeConstants.DOWNSTREAM"
+ @downstreamHovered="setJob"
+ @pipelineExpandToggle="togglePipelineExpanded"
+ @error="emit('error', errorType)"
+ />
+ </template>
+ </linked-graph-wrapper>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
new file mode 100644
index 00000000000..9ca4dc1e27a
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
@@ -0,0 +1,265 @@
+<script>
+import { escape, capitalize } from 'lodash';
+import { GlLoadingIcon } from '@gitlab/ui';
+import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
+import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
+import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
+import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
+
+export default {
+ name: 'PipelineGraphLegacy',
+ components: {
+ GlLoadingIcon,
+ LinkedPipelinesColumnLegacy,
+ StageColumnComponentLegacy,
+ },
+ mixins: [GraphBundleMixin],
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ isLinkedPipeline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ mediator: {
+ type: Object,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: false,
+ default: MAIN,
+ },
+ },
+ upstream: UPSTREAM,
+ downstream: DOWNSTREAM,
+ data() {
+ return {
+ downstreamMarginTop: null,
+ jobName: null,
+ pipelineExpanded: {
+ jobName: '',
+ expanded: false,
+ },
+ };
+ },
+ computed: {
+ graph() {
+ return this.pipeline.details?.stages;
+ },
+ hasUpstream() {
+ return (
+ this.type !== this.$options.downstream &&
+ this.upstreamPipelines &&
+ this.pipeline.triggered_by !== null
+ );
+ },
+ upstreamPipelines() {
+ return this.pipeline.triggered_by;
+ },
+ hasDownstream() {
+ return (
+ this.type !== this.$options.upstream &&
+ this.downstreamPipelines &&
+ this.pipeline.triggered.length > 0
+ );
+ },
+ downstreamPipelines() {
+ return this.pipeline.triggered;
+ },
+ expandedUpstream() {
+ return (
+ this.pipeline.triggered_by &&
+ Array.isArray(this.pipeline.triggered_by) &&
+ this.pipeline.triggered_by.find(el => el.isExpanded)
+ );
+ },
+ expandedDownstream() {
+ return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
+ },
+ pipelineTypeUpstream() {
+ return this.type !== this.$options.downstream && this.expandedUpstream;
+ },
+ pipelineTypeDownstream() {
+ return this.type !== this.$options.upstream && this.expandedDownstream;
+ },
+ pipelineProjectId() {
+ return this.pipeline.project.id;
+ },
+ },
+ methods: {
+ capitalizeStageName(name) {
+ const escapedName = escape(name);
+ return capitalize(escapedName);
+ },
+ isFirstColumn(index) {
+ return index === 0;
+ },
+ stageConnectorClass(index, stage) {
+ let className;
+
+ // If it's the first stage column and only has one job
+ if (this.isFirstColumn(index) && stage.groups.length === 1) {
+ className = 'no-margin';
+ } else if (index > 0) {
+ // If it is not the first column
+ className = 'left-margin';
+ }
+
+ return className;
+ },
+ refreshPipelineGraph() {
+ this.$emit('refreshPipelineGraph');
+ },
+ /**
+ * CSS class is applied:
+ * - if pipeline graph contains only one stage column component
+ *
+ * @param {number} index
+ * @returns {boolean}
+ */
+ shouldAddRightMargin(index) {
+ return !(index === this.graph.length - 1);
+ },
+ handleClickedDownstream(pipeline, clickedIndex, downstreamNode) {
+ /**
+ * Calculates the margin top of the clicked downstream pipeline by
+ * subtracting the clicked downstream pipelines offsetTop by it's parent's
+ * offsetTop and then subtracting 15
+ */
+ this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
+
+ /**
+ * If the expanded trigger is defined and the id is different than the
+ * pipeline we clicked, then it means we clicked on a sibling downstream link
+ * and we want to reset the pipeline store. Triggering the reset without
+ * this condition would mean not allowing downstreams of downstreams to expand
+ */
+ if (this.expandedDownstream?.id !== pipeline.id) {
+ this.$emit('onResetDownstream', this.pipeline, pipeline);
+ }
+
+ this.$emit('onClickDownstreamPipeline', pipeline);
+ },
+ calculateMarginTop(downstreamNode, pixelDiff) {
+ return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
+ },
+ hasOnlyOneJob(stage) {
+ return stage.groups.length === 1;
+ },
+ hasUpstreamColumn(index) {
+ return index === 0 && this.hasUpstream;
+ },
+ setJob(jobName) {
+ this.jobName = jobName;
+ },
+ setPipelineExpanded(jobName, expanded) {
+ if (expanded) {
+ this.pipelineExpanded = {
+ jobName,
+ expanded,
+ };
+ } else {
+ this.pipelineExpanded = {
+ expanded,
+ jobName: '',
+ };
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div class="build-content middle-block js-pipeline-graph">
+ <div
+ class="pipeline-visualization pipeline-graph"
+ :class="{ 'pipeline-tab-content': !isLinkedPipeline }"
+ >
+ <div class="gl-w-full">
+ <div class="container-fluid container-limited">
+ <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
+ <pipeline-graph-legacy
+ v-if="pipelineTypeUpstream"
+ :type="$options.upstream"
+ class="d-inline-block upstream-pipeline"
+ :class="`js-upstream-pipeline-${expandedUpstream.id}`"
+ :is-loading="false"
+ :pipeline="expandedUpstream"
+ :is-linked-pipeline="true"
+ :mediator="mediator"
+ @onClickUpstreamPipeline="clickUpstreamPipeline"
+ @refreshPipelineGraph="requestRefreshPipelineGraph"
+ />
+
+ <linked-pipelines-column-legacy
+ v-if="hasUpstream"
+ :type="$options.upstream"
+ :linked-pipelines="upstreamPipelines"
+ :column-title="__('Upstream')"
+ :project-id="pipelineProjectId"
+ @linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
+ />
+
+ <ul
+ v-if="!isLoading"
+ :class="{
+ 'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
+ }"
+ class="stage-column-list align-top"
+ >
+ <stage-column-component-legacy
+ v-for="(stage, index) in graph"
+ :key="stage.name"
+ :class="{
+ 'has-upstream gl-ml-11': hasUpstreamColumn(index),
+ 'has-only-one-job': hasOnlyOneJob(stage),
+ 'gl-mr-26': shouldAddRightMargin(index),
+ }"
+ :title="capitalizeStageName(stage.name)"
+ :groups="stage.groups"
+ :stage-connector-class="stageConnectorClass(index, stage)"
+ :is-first-column="isFirstColumn(index)"
+ :has-upstream="hasUpstream"
+ :action="stage.status.action"
+ :job-hovered="jobName"
+ :pipeline-expanded="pipelineExpanded"
+ @refreshPipelineGraph="refreshPipelineGraph"
+ />
+ </ul>
+
+ <linked-pipelines-column-legacy
+ v-if="hasDownstream"
+ :type="$options.downstream"
+ :linked-pipelines="downstreamPipelines"
+ :column-title="__('Downstream')"
+ :project-id="pipelineProjectId"
+ @linkedPipelineClick="handleClickedDownstream"
+ @downstreamHovered="setJob"
+ @pipelineExpandToggle="setPipelineExpanded"
+ />
+
+ <pipeline-graph-legacy
+ v-if="pipelineTypeDownstream"
+ :type="$options.downstream"
+ class="d-inline-block"
+ :class="`js-downstream-pipeline-${expandedDownstream.id}`"
+ :is-loading="false"
+ :pipeline="expandedDownstream"
+ :is-linked-pipeline="true"
+ :style="{ 'margin-top': downstreamMarginTop }"
+ :mediator="mediator"
+ @onClickDownstreamPipeline="clickDownstreamPipeline"
+ @refreshPipelineGraph="requestRefreshPipelineGraph"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
new file mode 100644
index 00000000000..d98e3aad054
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -0,0 +1,106 @@
+<script>
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { DEFAULT, LOAD_FAILURE } from '../../constants';
+import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
+import PipelineGraph from './graph_component.vue';
+import { unwrapPipelineData, toggleQueryPollingByVisibility } from './utils';
+
+export default {
+ name: 'PipelineGraphWrapper',
+ components: {
+ GlAlert,
+ GlLoadingIcon,
+ PipelineGraph,
+ },
+ inject: {
+ pipelineIid: {
+ default: '',
+ },
+ pipelineProjectPath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ pipeline: null,
+ alertType: null,
+ showAlert: false,
+ };
+ },
+ errorTexts: {
+ [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'),
+ [DEFAULT]: __('An unknown error occurred while loading this graph.'),
+ },
+ apollo: {
+ pipeline: {
+ query: getPipelineDetails,
+ pollInterval: 10000,
+ variables() {
+ return {
+ projectPath: this.pipelineProjectPath,
+ iid: this.pipelineIid,
+ };
+ },
+ update(data) {
+ return unwrapPipelineData(this.pipelineProjectPath, data);
+ },
+ error() {
+ this.reportFailure(LOAD_FAILURE);
+ },
+ },
+ },
+ computed: {
+ alert() {
+ switch (this.alertType) {
+ case LOAD_FAILURE:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE],
+ variant: 'danger',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT],
+ variant: 'danger',
+ };
+ }
+ },
+ showLoadingIcon() {
+ /*
+ Shows the icon only when the graph is empty, not when it is is
+ being refetched, for instance, on action completion
+ */
+ return this.$apollo.queries.pipeline.loading && !this.pipeline;
+ },
+ },
+ mounted() {
+ toggleQueryPollingByVisibility(this.$apollo.queries.pipeline);
+ },
+ methods: {
+ hideAlert() {
+ this.showAlert = false;
+ },
+ refreshPipelineGraph() {
+ this.$apollo.queries.pipeline.refetch();
+ },
+ reportFailure(type) {
+ this.showAlert = true;
+ this.failureType = type;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
+ {{ alert.text }}
+ </gl-alert>
+ <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
+ <pipeline-graph
+ v-if="pipeline"
+ :pipeline="pipeline"
+ @error="reportFailure"
+ @refreshPipelineGraph="refreshPipelineGraph"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 49591a80752..203d6a12edd 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -44,17 +44,18 @@ export default {
type="button"
data-toggle="dropdown"
data-display="static"
- class="dropdown-menu-toggle build-content"
+ class="dropdown-menu-toggle build-content gl-build-content"
>
- <ci-icon :status="group.status" />
+ <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ <span class="gl-display-flex gl-align-items-center gl-min-w-0">
+ <ci-icon :status="group.status" :size="24" />
+ <span class="gl-text-truncate mw-70p gl-pl-3">
+ {{ group.name }}
+ </span>
+ </span>
- <span
- class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom"
- >
- {{ group.name }}
- </span>
-
- <span class="dropdown-counter-badge"> {{ group.size }} </span>
+ <span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span>
+ </div>
</button>
<ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 4ed0aae0d1e..93ebe02d4e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -4,6 +4,8 @@ import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
import { sprintf } from '~/locale';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { accessValue } from './accessors';
+import { REST } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -41,6 +43,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [delayedJobMixin],
+ inject: {
+ dataMethod: {
+ default: REST,
+ },
+ },
props: {
job: {
type: Object,
@@ -71,10 +78,15 @@ export default {
boundary() {
return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
},
+ detailsPath() {
+ return accessValue(this.dataMethod, 'detailsPath', this.status);
+ },
+ hasDetails() {
+ return accessValue(this.dataMethod, 'hasDetails', this.status);
+ },
status() {
return this.job && this.job.status ? this.job.status : {};
},
-
tooltipText() {
const textBuilder = [];
const { name: jobName } = this.job;
@@ -129,19 +141,23 @@ export default {
};
</script>
<template>
- <div class="ci-job-component" data-qa-selector="job_item_container">
+ <div
+ class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
+ data-qa-selector="job_item_container"
+ >
<gl-link
- v-if="status.has_details"
+ v-if="hasDetails"
v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
- :href="status.details_path"
+ :href="detailsPath"
:title="tooltipText"
:class="jobClasses"
- class="js-pipeline-graph-job-link qa-job-link menu-item"
+ class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none
+ gl-focus-text-decoration-none gl-hover-text-decoration-none"
data-testid="job-with-link"
@click.stop="hideTooltips"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" />
+ <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
</gl-link>
<div
@@ -149,11 +165,11 @@ export default {
v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
:title="tooltipText"
:class="jobClasses"
- class="js-job-component-tooltip non-details-job-component"
+ class="js-job-component-tooltip non-details-job-component menu-item"
data-testid="job-without-link"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" />
+ <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
</div>
<action-component
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 1b71949784a..23a38fc053e 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -16,18 +16,22 @@ export default {
type: String,
required: true,
},
-
status: {
type: Object,
required: true,
},
+ iconSize: {
+ type: Number,
+ required: false,
+ default: 16,
+ },
},
};
</script>
<template>
- <span class="ci-job-name-component mw-100">
- <ci-icon :status="status" />
- <span class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom">
+ <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center">
+ <ci-icon :size="iconSize" :status="status" />
+ <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 11f06a25984..1a179de64cd 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -2,7 +2,8 @@
import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
-import { UPSTREAM, DOWNSTREAM } from './constants';
+import { accessValue } from './accessors';
+import { DOWNSTREAM, REST, UPSTREAM } from './constants';
export default {
directives: {
@@ -14,28 +15,43 @@ export default {
GlLink,
GlLoadingIcon,
},
+ inject: {
+ dataMethod: {
+ default: REST,
+ },
+ },
props: {
columnTitle: {
type: String,
required: true,
},
- pipeline: {
- type: Object,
+ expanded: {
+ type: Boolean,
required: true,
},
- projectId: {
- type: Number,
+ pipeline: {
+ type: Object,
required: true,
},
type: {
type: String,
required: true,
},
- },
- data() {
- return {
- expanded: false,
- };
+ /*
+ The next two props will be removed or required
+ once the graph transition is done.
+ See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
+ */
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectId: {
+ type: Number,
+ required: false,
+ default: -1,
+ },
},
computed: {
tooltipText() {
@@ -46,7 +62,7 @@ export default {
return `js-linked-pipeline-${this.pipeline.id}`;
},
pipelineStatus() {
- return this.pipeline.details.status;
+ return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline);
},
projectName() {
return this.pipeline.project.name;
@@ -68,6 +84,9 @@ export default {
}
return __('Multi-project');
},
+ pipelineIsLoading() {
+ return Boolean(this.isLoading || this.pipeline.isLoading);
+ },
isDownstream() {
return this.type === DOWNSTREAM;
},
@@ -75,12 +94,15 @@ export default {
return this.type === UPSTREAM;
},
isSameProject() {
- return this.projectId === this.pipeline.project.id;
+ return this.projectId > -1
+ ? this.projectId === this.pipeline.project.id
+ : !this.pipeline.multiproject;
+ },
+ sourceJobName() {
+ return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
},
sourceJobInfo() {
- return this.isDownstream
- ? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name })
- : '';
+ return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
},
expandedIcon() {
if (this.isUpstream) {
@@ -94,16 +116,15 @@ export default {
},
methods: {
onClickLinkedPipeline() {
- this.$root.$emit('bv::hide::tooltip', this.buttonId);
- this.expanded = !this.expanded;
+ this.hideTooltips();
this.$emit('pipelineClicked', this.$refs.linkedPipeline);
- this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded);
+ this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
},
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
},
onDownstreamHovered() {
- this.$emit('downstreamHovered', this.pipeline.source_job.name);
+ this.$emit('downstreamHovered', this.sourceJobName);
},
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
@@ -113,10 +134,10 @@ export default {
</script>
<template>
- <li
+ <div
ref="linkedPipeline"
v-gl-tooltip
- class="linked-pipeline build"
+ class="linked-pipeline build gl-pipeline-job-width"
:title="tooltipText"
:class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
@@ -129,8 +150,9 @@ export default {
>
<div class="gl-display-flex">
<ci-status
- v-if="!pipeline.isLoading"
+ v-if="!pipelineIsLoading"
:status="pipelineStatus"
+ :size="24"
css-classes="gl-top-0 gl-pr-2"
/>
<div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
@@ -153,10 +175,10 @@ export default {
class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`"
:icon="expandedIcon"
- data-testid="expandPipelineButton"
+ data-testid="expand-pipeline-button"
data-qa-selector="expand_pipeline_button"
@click="onClickLinkedPipeline"
/>
</div>
- </li>
+ </div>
</template>
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 2ca33e6d33e..7d333087874 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,10 +1,14 @@
<script>
+import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
import LinkedPipeline from './linked_pipeline.vue';
+import { LOAD_FAILURE } from '../../constants';
import { UPSTREAM } from './constants';
+import { unwrapPipelineData, toggleQueryPollingByVisibility } from './utils';
export default {
components: {
LinkedPipeline,
+ PipelineGraph: () => import('./graph_component.vue'),
},
props: {
columnTitle: {
@@ -19,11 +23,22 @@ export default {
type: String,
required: true,
},
- projectId: {
- type: Number,
- required: true,
- },
},
+ data() {
+ return {
+ currentPipeline: null,
+ loadingPipelineId: null,
+ pipelineExpanded: false,
+ };
+ },
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ 'gl-mb-5',
+ ],
computed: {
columnClass() {
const positionValues = {
@@ -35,14 +50,69 @@ export default {
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
- // Refactor string match when BE returns Upstream/Downstream indicators
isUpstream() {
return this.type === UPSTREAM;
},
+ computedTitleClasses() {
+ const positionalClasses = this.isUpstream
+ ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
+ : [];
+
+ return [...this.$options.titleClasses, ...positionalClasses];
+ },
},
methods: {
- onPipelineClick(downstreamNode, pipeline, index) {
- this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
+ getPipelineData(pipeline) {
+ const projectPath = pipeline.project.fullPath;
+
+ this.$apollo.addSmartQuery('currentPipeline', {
+ query: getPipelineDetails,
+ pollInterval: 10000,
+ variables() {
+ return {
+ projectPath,
+ iid: pipeline.iid,
+ };
+ },
+ update(data) {
+ return unwrapPipelineData(projectPath, data);
+ },
+ result() {
+ this.loadingPipelineId = null;
+ },
+ error() {
+ this.$emit('error', LOAD_FAILURE);
+ },
+ });
+
+ toggleQueryPollingByVisibility(this.$apollo.queries.currentPipeline);
+ },
+ isExpanded(id) {
+ return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id);
+ },
+ isLoadingPipeline(id) {
+ return this.loadingPipelineId === id;
+ },
+ onPipelineClick(pipeline) {
+ /* If the clicked pipeline has been expanded already, close it, clear, exit */
+ if (this.currentPipeline?.id === pipeline.id) {
+ this.pipelineExpanded = false;
+ this.currentPipeline = null;
+ return;
+ }
+
+ /* Set the loading id */
+ this.loadingPipelineId = pipeline.id;
+
+ /*
+ Expand the pipeline.
+ If this was not a toggle close action, and
+ it was already showing a different pipeline, then
+ this will be a no-op, but that doesn't matter.
+ */
+ this.pipelineExpanded = true;
+
+ this.getPipelineData(pipeline);
},
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
@@ -60,25 +130,40 @@ export default {
</script>
<template>
- <div :class="columnClass" class="stage-column linked-pipelines-column">
- <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
- <div v-if="isUpstream" class="cross-project-triangle"></div>
- <ul>
- <linked-pipeline
- v-for="(pipeline, index) in linkedPipelines"
- :key="pipeline.id"
- :class="{
- active: pipeline.isExpanded,
- 'left-connector': pipeline.isExpanded && graphPosition === 'left',
- }"
- :pipeline="pipeline"
- :column-title="columnTitle"
- :project-id="projectId"
- :type="type"
- @pipelineClicked="onPipelineClick($event, pipeline, index)"
- @downstreamHovered="onDownstreamHovered"
- @pipelineExpandToggle="onPipelineExpandToggle"
- />
- </ul>
+ <div class="gl-display-flex">
+ <div :class="columnClass" class="linked-pipelines-column">
+ <div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses">
+ {{ columnTitle }}
+ </div>
+ <ul class="gl-pl-0">
+ <li
+ v-for="pipeline in linkedPipelines"
+ :key="pipeline.id"
+ class="gl-display-flex gl-mb-4"
+ :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
+ >
+ <linked-pipeline
+ class="gl-display-inline-block"
+ :is-loading="isLoadingPipeline(pipeline.id)"
+ :pipeline="pipeline"
+ :column-title="columnTitle"
+ :type="type"
+ :expanded="isExpanded(pipeline.id)"
+ @downstreamHovered="onDownstreamHovered"
+ @pipelineClicked="onPipelineClick(pipeline)"
+ @pipelineExpandToggle="onPipelineExpandToggle"
+ />
+ <div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block">
+ <pipeline-graph
+ v-if="currentPipeline"
+ :type="type"
+ class="d-inline-block gl-mt-n2"
+ :pipeline="currentPipeline"
+ :is-linked-pipeline="true"
+ />
+ </div>
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
new file mode 100644
index 00000000000..7d371b33220
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
@@ -0,0 +1,87 @@
+<script>
+import LinkedPipeline from './linked_pipeline.vue';
+import { UPSTREAM } from './constants';
+
+export default {
+ components: {
+ LinkedPipeline,
+ },
+ props: {
+ columnTitle: {
+ type: String,
+ required: true,
+ },
+ linkedPipelines: {
+ type: Array,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ columnClass() {
+ const positionValues = {
+ right: 'gl-ml-11',
+ left: 'gl-mr-7',
+ };
+ return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
+ },
+ graphPosition() {
+ return this.isUpstream ? 'left' : 'right';
+ },
+ isExpanded() {
+ return this.pipeline?.isExpanded || false;
+ },
+ isUpstream() {
+ return this.type === UPSTREAM;
+ },
+ },
+ methods: {
+ onPipelineClick(downstreamNode, pipeline, index) {
+ this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
+ },
+ onDownstreamHovered(jobName) {
+ this.$emit('downstreamHovered', jobName);
+ },
+ onPipelineExpandToggle(jobName, expanded) {
+ // Highlighting only applies to downstream pipelines
+ if (this.isUpstream) {
+ return;
+ }
+
+ this.$emit('pipelineExpandToggle', jobName, expanded);
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="columnClass" class="stage-column linked-pipelines-column">
+ <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
+ <div v-if="isUpstream" class="cross-project-triangle"></div>
+ <ul>
+ <li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id">
+ <linked-pipeline
+ :class="{
+ active: pipeline.isExpanded,
+ 'left-connector': pipeline.isExpanded && graphPosition === 'left',
+ }"
+ :pipeline="pipeline"
+ :column-title="columnTitle"
+ :project-id="projectId"
+ :type="type"
+ :expanded="isExpanded"
+ @pipelineClicked="onPipelineClick($event, pipeline, index)"
+ @downstreamHovered="onDownstreamHovered"
+ @pipelineExpandToggle="onPipelineExpandToggle"
+ />
+ </li>
+ </ul>
+ </div>
+</template>
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 a75ec585b95..b9bddc94ce4 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,17 +1,19 @@
<script>
-import { isEmpty, escape } from 'lodash';
-import stageColumnMixin from '../../mixins/stage_column_mixin';
+import { capitalize, escape, isEmpty } from 'lodash';
+import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
import JobItem from './job_item.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
import ActionComponent from './action_component.vue';
+import { GRAPHQL } from './constants';
+import { accessValue } from './accessors';
export default {
components: {
- JobItem,
- JobGroupDropdown,
ActionComponent,
+ JobGroupDropdown,
+ JobItem,
+ MainGraphWrapper,
},
- mixins: [stageColumnMixin],
props: {
title: {
type: String,
@@ -21,16 +23,6 @@ export default {
type: Array,
required: true,
},
- isFirstColumn: {
- type: Boolean,
- required: false,
- default: false,
- },
- stageConnectorClass: {
- type: String,
- required: false,
- default: '',
- },
action: {
type: Object,
required: false,
@@ -47,62 +39,68 @@ export default {
default: () => ({}),
},
},
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ ],
computed: {
+ formattedTitle() {
+ return capitalize(escape(this.title));
+ },
hasAction() {
return !isEmpty(this.action);
},
},
methods: {
+ getGroupId(group) {
+ return accessValue(GRAPHQL, 'groupId', group);
+ },
groupId(group) {
return `ci-badge-${escape(group.name)}`;
},
- pipelineActionRequestComplete() {
- this.$emit('refreshPipelineGraph');
- },
},
};
</script>
<template>
- <li :class="stageConnectorClass" class="stage-column">
- <div class="stage-name position-relative">
- {{ title }}
- <action-component
- v-if="hasAction"
- :action-icon="action.icon"
- :tooltip-text="action.title"
- :link="action.path"
- class="js-stage-action stage-action rounded"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </div>
-
- <div class="builds-container">
- <ul>
- <li
- v-for="(group, index) in groups"
- :id="groupId(group)"
- :key="group.id"
- :class="buildConnnectorClass(index)"
- class="build"
- >
- <div class="curve"></div>
-
- <job-item
- v-if="group.size === 1"
- :job="group.jobs[0]"
- :job-hovered="jobHovered"
- :pipeline-expanded="pipelineExpanded"
- css-class-job-name="build-content"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
-
- <job-group-dropdown
- v-if="group.size > 1"
- :group="group"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </li>
- </ul>
- </div>
- </li>
+ <main-graph-wrapper>
+ <template #stages>
+ <div
+ data-testid="stage-column-title"
+ class="gl-display-flex gl-justify-content-space-between gl-relative"
+ :class="$options.titleClasses"
+ >
+ <div>{{ formattedTitle }}</div>
+ <action-component
+ v-if="hasAction"
+ :action-icon="action.icon"
+ :tooltip-text="action.title"
+ :link="action.path"
+ class="js-stage-action stage-action rounded"
+ @pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
+ />
+ </div>
+ </template>
+ <template #jobs>
+ <div
+ v-for="group in groups"
+ :id="groupId(group)"
+ :key="getGroupId(group)"
+ data-testid="stage-column-group"
+ class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ >
+ <job-item
+ v-if="group.size === 1"
+ :job="group.jobs[0]"
+ :job-hovered="jobHovered"
+ :pipeline-expanded="pipelineExpanded"
+ css-class-job-name="gl-build-content"
+ @pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
+ />
+ <job-group-dropdown v-else :group="group" />
+ </div>
+ </template>
+ </main-graph-wrapper>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
new file mode 100644
index 00000000000..258b6bf6b6d
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
@@ -0,0 +1,108 @@
+<script>
+import { isEmpty, escape } from 'lodash';
+import stageColumnMixin from '../../mixins/stage_column_mixin';
+import JobItem from './job_item.vue';
+import JobGroupDropdown from './job_group_dropdown.vue';
+import ActionComponent from './action_component.vue';
+
+export default {
+ components: {
+ JobItem,
+ JobGroupDropdown,
+ ActionComponent,
+ },
+ mixins: [stageColumnMixin],
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ groups: {
+ type: Array,
+ required: true,
+ },
+ isFirstColumn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ stageConnectorClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ action: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ jobHovered: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pipelineExpanded: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ hasAction() {
+ return !isEmpty(this.action);
+ },
+ },
+ methods: {
+ groupId(group) {
+ return `ci-badge-${escape(group.name)}`;
+ },
+ pipelineActionRequestComplete() {
+ this.$emit('refreshPipelineGraph');
+ },
+ },
+};
+</script>
+<template>
+ <li :class="stageConnectorClass" class="stage-column">
+ <div class="stage-name position-relative" data-testid="stage-column-title">
+ {{ title }}
+ <action-component
+ v-if="hasAction"
+ :action-icon="action.icon"
+ :tooltip-text="action.title"
+ :link="action.path"
+ class="js-stage-action stage-action rounded"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </div>
+
+ <div class="builds-container">
+ <ul>
+ <li
+ v-for="(group, index) in groups"
+ :id="groupId(group)"
+ :key="group.id"
+ :class="buildConnnectorClass(index)"
+ class="build"
+ >
+ <div class="curve"></div>
+
+ <job-item
+ v-if="group.size === 1"
+ :job="group.jobs[0]"
+ :job-hovered="jobHovered"
+ :pipeline-expanded="pipelineExpanded"
+ css-class-job-name="build-content"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+
+ <job-group-dropdown
+ v-if="group.size > 1"
+ :group="group"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
+ </ul>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
new file mode 100644
index 00000000000..32588feb426
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -0,0 +1,57 @@
+import Visibility from 'visibilityjs';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { unwrapStagesWithNeeds } from '../unwrapping_utils';
+
+const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
+ return {
+ ...linkedPipeline,
+ multiproject: mainPipelineProjectPath !== linkedPipeline.project.fullPath,
+ };
+};
+
+const transformId = linkedPipeline => {
+ return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
+};
+
+const unwrapPipelineData = (mainPipelineProjectPath, data) => {
+ if (!data?.project?.pipeline) {
+ return null;
+ }
+
+ const { pipeline } = data.project;
+
+ const {
+ upstream,
+ downstream,
+ stages: { nodes: stages },
+ } = pipeline;
+
+ const nodes = unwrapStagesWithNeeds(stages);
+
+ return {
+ ...pipeline,
+ id: getIdFromGraphQLId(pipeline.id),
+ stages: nodes,
+ upstream: upstream
+ ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
+ : [],
+ downstream: downstream
+ ? downstream.nodes.map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
+ : [],
+ };
+};
+
+const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
+ const stopStartQuery = query => {
+ if (!Visibility.hidden()) {
+ query.startPolling(interval);
+ } else {
+ query.stopPolling();
+ }
+ };
+
+ stopStartQuery(queryRef);
+ Visibility.change(stopStartQuery.bind(null, queryRef));
+};
+
+export { unwrapPipelineData, toggleQueryPollingByVisibility };