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/graph_component.vue169
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue65
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue52
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue2
4 files changed, 282 insertions, 6 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index cfc72327ef7..e29509ce074 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,20 +1,120 @@
<script>
+import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponent from './stage_column_component.vue';
import GraphMixin from '../../mixins/graph_component_mixin';
-import GraphWidthMixin from '~/pipelines/mixins/graph_width_mixin';
+import GraphWidthMixin from '../../mixins/graph_width_mixin';
+import LinkedPipelinesColumn from './linked_pipelines_column.vue';
+import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
export default {
+ name: 'PipelineGraph',
components: {
StageColumnComponent,
GlLoadingIcon,
+ LinkedPipelinesColumn,
+ },
+ mixins: [GraphMixin, GraphWidthMixin, 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 {
+ triggeredTopIndex: 1,
+ };
+ },
+ computed: {
+ hasTriggeredBy() {
+ return (
+ this.type !== this.$options.downstream &&
+ this.triggeredByPipelines &&
+ this.pipeline.triggered_by !== null
+ );
+ },
+ triggeredByPipelines() {
+ return this.pipeline.triggered_by;
+ },
+ hasTriggered() {
+ return (
+ this.type !== this.$options.upstream &&
+ this.triggeredPipelines &&
+ this.pipeline.triggered.length > 0
+ );
+ },
+ triggeredPipelines() {
+ return this.pipeline.triggered;
+ },
+ expandedTriggeredBy() {
+ return (
+ this.pipeline.triggered_by &&
+ _.isArray(this.pipeline.triggered_by) &&
+ this.pipeline.triggered_by.find(el => el.isExpanded)
+ );
+ },
+ expandedTriggered() {
+ return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
+ },
+
+ /**
+ * Calculates the margin top of the clicked downstream pipeline by
+ * adding the height of each linked pipeline and the margin
+ */
+ marginTop() {
+ return `${this.triggeredTopIndex * 52}px`;
+ },
+ pipelineTypeUpstream() {
+ return this.type !== this.$options.downstream && this.expandedTriggeredBy;
+ },
+ pipelineTypeDownstream() {
+ return this.type !== this.$options.upstream && this.expandedTriggered;
+ },
+ },
+ methods: {
+ handleClickedDownstream(pipeline, clickedIndex) {
+ this.triggeredTopIndex = clickedIndex;
+ this.$emit('onClickTriggered', this.pipeline, pipeline);
+ },
+ hasOnlyOneJob(stage) {
+ return stage.groups.length === 1;
+ },
+ hasDownstream(index, length) {
+ return index === length - 1 && this.hasTriggered;
+ },
+ hasUpstream(index) {
+ return index === 0 && this.hasTriggeredBy;
+ },
},
- mixins: [GraphMixin, GraphWidthMixin],
};
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
- <div class="pipeline-visualization pipeline-graph pipeline-tab-content">
+ <div
+ class="pipeline-visualization pipeline-graph"
+ :class="{ 'pipeline-tab-content': !isLinkedPipeline }"
+ >
<div
:style="{
paddingLeft: `${graphLeftPadding}px`,
@@ -23,21 +123,80 @@ export default {
>
<gl-loading-icon v-if="isLoading" class="m-auto" :size="3" />
- <ul v-if="!isLoading" class="stage-column-list">
+ <pipeline-graph
+ v-if="pipelineTypeUpstream"
+ type="upstream"
+ class="d-inline-block upstream-pipeline"
+ :class="`js-upstream-pipeline-${expandedTriggeredBy.id}`"
+ :is-loading="false"
+ :pipeline="expandedTriggeredBy"
+ :is-linked-pipeline="true"
+ :mediator="mediator"
+ @onClickTriggeredBy="
+ (parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline)
+ "
+ @refreshPipelineGraph="requestRefreshPipelineGraph"
+ />
+
+ <linked-pipelines-column
+ v-if="hasTriggeredBy"
+ :linked-pipelines="triggeredByPipelines"
+ :column-title="__('Upstream')"
+ graph-position="left"
+ @linkedPipelineClick="
+ linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
+ "
+ />
+
+ <ul
+ v-if="!isLoading"
+ :class="{
+ 'inline js-has-linked-pipelines': hasTriggered || hasTriggeredBy,
+ }"
+ class="stage-column-list align-top"
+ >
<stage-column-component
v-for="(stage, index) in graph"
:key="stage.name"
:class="{
- 'append-right-48': shouldAddRightMargin(index),
+ 'has-upstream prepend-left-64': hasUpstream(index),
+ 'has-downstream': hasDownstream(index, graph.length),
+ 'has-only-one-job': hasOnlyOneJob(stage),
+ 'append-right-46': shouldAddRightMargin(index),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
+ :has-triggered-by="hasTriggeredBy"
:action="stage.status.action"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
+
+ <linked-pipelines-column
+ v-if="hasTriggered"
+ :linked-pipelines="triggeredPipelines"
+ :column-title="__('Downstream')"
+ graph-position="right"
+ @linkedPipelineClick="handleClickedDownstream"
+ />
+
+ <pipeline-graph
+ v-if="pipelineTypeDownstream"
+ type="downstream"
+ class="d-inline-block"
+ :class="`js-downstream-pipeline-${expandedTriggered.id}`"
+ :is-loading="false"
+ :pipeline="expandedTriggered"
+ :is-linked-pipeline="true"
+ :style="{ 'margin-top': marginTop }"
+ :mediator="mediator"
+ @onClickTriggered="
+ (parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
+ "
+ @refreshPipelineGraph="requestRefreshPipelineGraph"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
new file mode 100644
index 00000000000..4e7d77863b9
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlLoadingIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import CiStatus from '~/vue_shared/components/ci_icon.vue';
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ CiStatus,
+ GlLoadingIcon,
+ GlButton,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipText() {
+ return `${this.projectName} - ${this.pipelineStatus.label}`;
+ },
+ buttonId() {
+ return `js-linked-pipeline-${this.pipeline.id}`;
+ },
+ pipelineStatus() {
+ return this.pipeline.details.status;
+ },
+ projectName() {
+ return this.pipeline.project.name;
+ },
+ },
+ methods: {
+ onClickLinkedPipeline() {
+ this.$root.$emit('bv::hide::tooltip', this.buttonId);
+ this.$emit('pipelineClicked');
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="linked-pipeline build">
+ <div class="curve"></div>
+ <gl-button
+ :id="buttonId"
+ v-gl-tooltip
+ :title="tooltipText"
+ class="js-linked-pipeline-content linked-pipeline-content"
+ data-qa-selector="linked_pipeline_button"
+ :class="`js-pipeline-expand-${pipeline.id}`"
+ @click="onClickLinkedPipeline"
+ >
+ <gl-loading-icon v-if="pipeline.isLoading" class="js-linked-pipeline-loading d-inline" />
+ <ci-status
+ v-else
+ :status="pipelineStatus"
+ css-classes="position-top-0"
+ class="js-linked-pipeline-status"
+ />
+ <span class="str-truncated align-bottom"> {{ projectName }} &#8226; #{{ pipeline.id }} </span>
+ </gl-button>
+ </li>
+</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
new file mode 100644
index 00000000000..6efdde2b17e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -0,0 +1,52 @@
+<script>
+import LinkedPipeline from './linked_pipeline.vue';
+
+export default {
+ components: {
+ LinkedPipeline,
+ },
+ props: {
+ columnTitle: {
+ type: String,
+ required: true,
+ },
+ linkedPipelines: {
+ type: Array,
+ required: true,
+ },
+ graphPosition: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ columnClass() {
+ const positionValues = {
+ right: 'prepend-left-64',
+ left: 'append-right-32',
+ };
+ return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="columnClass" class="stage-column linked-pipelines-column">
+ <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
+ <div class="cross-project-triangle"></div>
+ <ul>
+ <linked-pipeline
+ v-for="(pipeline, index) in linkedPipelines"
+ :key="pipeline.id"
+ :class="{
+ 'flat-connector-before': index === 0 && graphPosition === 'right',
+ active: pipeline.isExpanded,
+ 'left-connector': pipeline.isExpanded && graphPosition === 'left',
+ }"
+ :pipeline="pipeline"
+ @pipelineClicked="$emit('linkedPipelineClick', pipeline, index)"
+ />
+ </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 d5c124dc0ca..db7714808fd 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import stageColumnMixin from 'ee_else_ce/pipelines/mixins/stage_column_mixin';
+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';