summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines
diff options
context:
space:
mode:
authorRobert Speicher <rspeicher@gmail.com>2021-01-20 13:34:23 -0600
committerRobert Speicher <rspeicher@gmail.com>2021-01-20 13:34:23 -0600
commit6438df3a1e0fb944485cebf07976160184697d72 (patch)
tree00b09bfd170e77ae9391b1a2f5a93ef6839f2597 /app/assets/javascripts/pipelines
parent42bcd54d971da7ef2854b896a7b34f4ef8601067 (diff)
downloadgitlab-ce-6438df3a1e0fb944485cebf07976160184697d72.tar.gz
Add latest changes from gitlab-org/gitlab@13-8-stable-eev13.8.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipelines')
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/dag/drawing_utils.js4
-rw-r--r--app/assets/javascripts/pipelines/components/dag/interactions.js28
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue78
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue16
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue41
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js12
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js)37
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue140
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue86
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js24
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue114
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue54
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue68
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue52
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/unwrapping_utils.js33
-rw-r--r--app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql17
-rw-r--r--app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql)14
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql65
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js19
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_graph.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_header.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js27
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js10
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/index.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js2
-rw-r--r--app/assets/javascripts/pipelines/utils.js32
49 files changed, 737 insertions, 386 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 85171263f08..2482af2c7f0 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -56,15 +56,15 @@ export default {
const unwrappedGroups = stages
.map(({ name, groups: { nodes: groups } }) => {
- return groups.map(group => {
+ return groups.map((group) => {
return { category: name, ...group };
});
})
.flat(2);
- const nodes = unwrappedGroups.map(group => {
+ const nodes = unwrappedGroups.map((group) => {
const jobs = group.jobs.nodes.map(({ name, needs }) => {
- return { name, needs: needs.nodes.map(need => need.name) };
+ return { name, needs: needs.nodes.map((need) => need.name) };
});
return { ...group, jobs };
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
index 42d1debcddf..5ba0604fa01 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
@@ -173,7 +173,7 @@ export default {
createClip(link) {
return link
.append('clipPath')
- .attr('id', d => {
+ .attr('id', (d) => {
return this.createAndAssignId(d, 'clipId', 'dag-clip');
})
.append('path')
@@ -183,7 +183,7 @@ export default {
createGradient(link) {
const gradient = link
.append('linearGradient')
- .attr('id', d => {
+ .attr('id', (d) => {
return this.createAndAssignId(d, 'gradId', 'dag-grad');
})
.attr('gradientUnits', 'userSpaceOnUse')
@@ -251,7 +251,7 @@ export default {
.data(linksData)
.enter()
.append('g')
- .attr('id', d => {
+ .attr('id', (d) => {
return this.createAndAssignId(d, 'uid', LINK_SELECTOR);
})
.classed(
@@ -273,10 +273,10 @@ export default {
`${NODE_SELECTOR} gl-transition-property-stroke ${this.$options.viewOptions.hoverFadeClasses}`,
true,
)
- .attr('id', d => {
+ .attr('id', (d) => {
return this.createAndAssignId(d, 'uid', NODE_SELECTOR);
})
- .attr('stroke', d => {
+ .attr('stroke', (d) => {
const color = this.color(d);
/* eslint-disable-next-line no-param-reassign */
d.color = color;
@@ -284,10 +284,10 @@ export default {
})
.attr('stroke-width', nodeWidth)
.attr('stroke-linecap', 'round')
- .attr('x1', d => Math.floor((d.x1 + d.x0) / 2))
- .attr('x2', d => Math.floor((d.x1 + d.x0) / 2))
- .attr('y1', d => d.y0 + 4)
- .attr('y2', d => d.y1 - 4);
+ .attr('x1', (d) => Math.floor((d.x1 + d.x0) / 2))
+ .attr('x2', (d) => Math.floor((d.x1 + d.x0) / 2))
+ .attr('y1', (d) => d.y0 + 4)
+ .attr('y2', (d) => d.y1 - 4);
},
initColors() {
diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
index d56addc473f..3cd09d57ffb 100644
--- a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
@@ -92,8 +92,8 @@ export const createSankey = ({
]);
return ({ nodes, links }) =>
sankeyGenerator({
- nodes: nodes.map(d => ({ ...d })),
- links: links.map(d => ({ ...d })),
+ nodes: nodes.map((d) => ({ ...d })),
+ links: links.map((d) => ({ ...d })),
});
};
diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/pipelines/components/dag/interactions.js
index e9f3e9f0e2c..69f36feeee4 100644
--- a/app/assets/javascripts/pipelines/components/dag/interactions.js
+++ b/app/assets/javascripts/pipelines/components/dag/interactions.js
@@ -13,22 +13,22 @@ export const getLiveLinksAsDict = () => {
return Object.fromEntries(
getLiveLinks()
.data()
- .map(d => [d.uid, d]),
+ .map((d) => [d.uid, d]),
);
};
export const currentIsLive = (idx, collection) =>
getCurrent(idx, collection).classed(IS_HIGHLIGHTED);
-const backgroundLinks = selection => selection.style('stroke-opacity', highlightOut);
-const backgroundNodes = selection => selection.attr('stroke', '#f2f2f2');
-const foregroundLinks = selection => selection.style('stroke-opacity', highlightIn);
-const foregroundNodes = selection => selection.attr('stroke', d => d.color);
+const backgroundLinks = (selection) => selection.style('stroke-opacity', highlightOut);
+const backgroundNodes = (selection) => selection.attr('stroke', '#f2f2f2');
+const foregroundLinks = (selection) => selection.style('stroke-opacity', highlightIn);
+const foregroundNodes = (selection) => selection.attr('stroke', (d) => d.color);
const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity);
-const renewNodes = selection => selection.attr('stroke', d => d.color);
+const renewNodes = (selection) => selection.attr('stroke', (d) => d.color);
-export const getAllLinkAncestors = node => {
+export const getAllLinkAncestors = (node) => {
if (node.targetLinks) {
- return node.targetLinks.flatMap(n => {
+ return node.targetLinks.flatMap((n) => {
return [n, ...getAllLinkAncestors(n.source)];
});
}
@@ -36,11 +36,11 @@ export const getAllLinkAncestors = node => {
return [];
};
-const getAllNodeAncestors = node => {
+const getAllNodeAncestors = (node) => {
let allNodes = [];
if (node.targetLinks) {
- allNodes = node.targetLinks.flatMap(n => {
+ allNodes = node.targetLinks.flatMap((n) => {
return getAllNodeAncestors(n.source);
});
}
@@ -74,7 +74,7 @@ const highlightPath = (parentLinks, parentNodes) => {
});
/* highlight correct nodes */
- parentNodes.forEach(id => {
+ parentNodes.forEach((id) => {
foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true);
});
};
@@ -86,7 +86,7 @@ const restoreNodes = () => {
rehighlights their nodes.
*/
- getLiveLinks().each(d => {
+ getLiveLinks().each((d) => {
foregroundNodes(d3.select(`#${d.source.uid}`)).classed(IS_HIGHLIGHTED, true);
foregroundNodes(d3.select(`#${d.target.uid}`)).classed(IS_HIGHLIGHTED, true);
});
@@ -97,7 +97,7 @@ const restorePath = (parentLinks, parentNodes, baseOpacity) => {
renewLinks(d3.select(`#${uid}`), baseOpacity).classed(IS_HIGHLIGHTED, false);
});
- parentNodes.forEach(id => {
+ parentNodes.forEach((id) => {
d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false);
});
@@ -112,7 +112,7 @@ const restorePath = (parentLinks, parentNodes, baseOpacity) => {
restoreNodes();
};
-export const restoreLinks = baseOpacity => {
+export const restoreLinks = (baseOpacity) => {
/*
if there exist live links, reset to highlight out / pale
otherwise, reset to base
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 4e9b21a5c55..0ce94d4f02f 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { reportToSentry } from './utils';
/**
* Renders either a cancel, retry or play icon button and handles the post request
@@ -50,6 +51,9 @@ export default {
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('action_component', `error: ${err}, info: ${info}`);
+ },
methods: {
/**
* The request should not be handled here.
@@ -70,10 +74,12 @@ export default {
this.$emit('pipelineActionRequestComplete');
})
- .catch(() => {
+ .catch((err) => {
this.isDisabled = false;
this.isLoading = false;
+ reportToSentry('action_component', err);
+
createFlash(__('An error occurred while making the request.'));
});
},
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 67b2ed3b596..cd403757fe6 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,12 +1,15 @@
<script>
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
+import LinksLayer from '../graph_shared/links_layer.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
+import { reportToSentry } from './utils';
export default {
name: 'PipelineGraph',
components: {
+ LinksLayer,
LinkedGraphWrapper,
LinkedPipelinesColumn,
StageColumnComponent,
@@ -31,9 +34,16 @@ export default {
DOWNSTREAM,
UPSTREAM,
},
+ CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF',
+ BASE_CONTAINER_ID: 'pipeline-links-container',
data() {
return {
hoveredJobName: '',
+ highlightedJobs: [],
+ measurements: {
+ width: 0,
+ height: 0,
+ },
pipelineExpanded: {
jobName: '',
expanded: false,
@@ -41,6 +51,9 @@ export default {
};
},
computed: {
+ containerId() {
+ return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`;
+ },
downstreamPipelines() {
return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
},
@@ -53,12 +66,13 @@ export default {
hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0);
},
- // The two show checks prevent upstream / downstream from showing redundant linked columns
+ // The show downstream check prevents showing redundant linked columns
showDownstreamPipelines() {
return (
this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
);
},
+ // The show upstream check prevents showing redundant linked columns
showUpstreamPipelines() {
return (
this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
@@ -68,7 +82,22 @@ export default {
return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
+ },
+ mounted() {
+ this.measurements = this.getMeasurements();
+ },
methods: {
+ getMeasurements() {
+ return {
+ width: this.$refs[this.containerId].scrollWidth,
+ height: this.$refs[this.containerId].scrollHeight,
+ };
+ },
+ onError(errorType) {
+ this.$emit('error', errorType);
+ },
setJob(jobName) {
this.hoveredJobName = jobName;
},
@@ -78,14 +107,17 @@ export default {
jobName: expanded ? jobName : '',
};
},
+ updateHighlightedJobs(jobs) {
+ this.highlightedJobs = jobs;
+ },
},
};
</script>
<template>
<div class="js-pipeline-graph">
<div
- 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 }"
+ class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
+ :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }"
>
<linked-graph-wrapper>
<template #upstream>
@@ -94,20 +126,36 @@ export default {
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:type="$options.pipelineTypeConstants.UPSTREAM"
- @error="emit('error', errorType)"
+ @error="onError"
/>
</template>
<template #main>
- <stage-column-component
- v-for="stage in graph"
- :key="stage.name"
- :title="stage.name"
- :groups="stage.groups"
- :action="stage.status.action"
- :job-hovered="hoveredJobName"
- :pipeline-expanded="pipelineExpanded"
- @refreshPipelineGraph="$emit('refreshPipelineGraph')"
- />
+ <div :id="containerId" :ref="containerId">
+ <links-layer
+ :pipeline-data="graph"
+ :pipeline-id="pipeline.id"
+ :container-id="containerId"
+ :container-measurements="measurements"
+ :highlighted-job="hoveredJobName"
+ default-link-color="gl-stroke-transparent"
+ @error="onError"
+ @highlightedJobsChange="updateHighlightedJobs"
+ >
+ <stage-column-component
+ v-for="stage in graph"
+ :key="stage.name"
+ :title="stage.name"
+ :groups="stage.groups"
+ :action="stage.status.action"
+ :highlighted-jobs="highlightedJobs"
+ :job-hovered="hoveredJobName"
+ :pipeline-expanded="pipelineExpanded"
+ :pipeline-id="pipeline.id"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
+ @jobHover="setJob"
+ />
+ </links-layer>
+ </div>
</template>
<template #downstream>
<linked-pipelines-column
@@ -117,7 +165,7 @@ export default {
:type="$options.pipelineTypeConstants.DOWNSTREAM"
@downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded"
- @error="emit('error', errorType)"
+ @error="onError"
/>
</template>
</linked-graph-wrapper>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
index 9ca4dc1e27a..2164dbf4d55 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
@@ -5,6 +5,7 @@ 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';
+import { reportToSentry } from './utils';
export default {
name: 'PipelineGraphLegacy',
@@ -78,11 +79,11 @@ export default {
return (
this.pipeline.triggered_by &&
Array.isArray(this.pipeline.triggered_by) &&
- this.pipeline.triggered_by.find(el => el.isExpanded)
+ this.pipeline.triggered_by.find((el) => el.isExpanded)
);
},
expandedDownstream() {
- return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
+ return this.pipeline.triggered && this.pipeline.triggered.find((el) => el.isExpanded);
},
pipelineTypeUpstream() {
return this.type !== this.$options.downstream && this.expandedUpstream;
@@ -94,6 +95,9 @@ export default {
return this.pipeline.project.id;
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
+ },
methods: {
capitalizeStageName(name) {
const escapedName = escape(name);
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 d98e3aad054..f596333237d 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -1,10 +1,10 @@
<script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
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';
+import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
export default {
name: 'PipelineGraphWrapper',
@@ -76,6 +76,9 @@ export default {
mounted() {
toggleQueryPollingByVisibility(this.$apollo.queries.pipeline);
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
+ },
methods: {
hideAlert() {
this.showAlert = false;
@@ -86,6 +89,7 @@ export default {
reportFailure(type) {
this.showAlert = true;
this.failureType = type;
+ reportToSentry(this.$options.name, this.failureType);
},
},
};
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 203d6a12edd..08d6162aeb8 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -2,6 +2,7 @@
import { GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobItem from './job_item.vue';
+import { reportToSentry } from './utils';
/**
* Renders the dropdown for the pipeline graph.
@@ -22,13 +23,24 @@ export default {
type: Object,
required: true,
},
+ pipelineId: {
+ type: Number,
+ required: false,
+ default: -1,
+ },
},
computed: {
+ computedJobId() {
+ return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : '';
+ },
tooltipText() {
const { name, status } = this.group;
return `${name} - ${status.label}`;
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('job_group_dropdown', `error: ${err}, info: ${info}`);
+ },
methods: {
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
@@ -37,7 +49,7 @@ export default {
};
</script>
<template>
- <div class="ci-job-dropdown-container dropdown dropright">
+ <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright">
<button
v-gl-tooltip.hover="{ boundary: 'viewport' }"
:title="tooltipText"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 93ebe02d4e8..8262d728a24 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -6,6 +6,7 @@ import { sprintf } from '~/locale';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { accessValue } from './accessors';
import { REST } from './constants';
+import { reportToSentry } from './utils';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -73,6 +74,11 @@ export default {
required: false,
default: () => ({}),
},
+ pipelineId: {
+ type: Number,
+ required: false,
+ default: -1,
+ },
},
computed: {
boundary() {
@@ -84,6 +90,9 @@ export default {
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
},
+ computedJobId() {
+ return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
+ },
status() {
return this.job && this.job.status ? this.job.status : {};
},
@@ -130,6 +139,9 @@ export default {
: this.cssClassJobName;
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('job_item', `error: ${err}, info: ${info}`);
+ },
methods: {
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
@@ -142,6 +154,7 @@ export default {
</script>
<template>
<div
+ :id="computedJobId"
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
data-qa-selector="job_item_container"
>
@@ -151,8 +164,7 @@ export default {
:href="detailsPath"
:title="tooltipText"
:class="jobClasses"
- 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"
+ 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"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 1a179de64cd..d18e604f087 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,9 +1,10 @@
<script>
-import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
import { accessValue } from './accessors';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
+import { reportToSentry } from './utils';
export default {
directives: {
@@ -14,6 +15,7 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
+ GlBadge,
},
inject: {
dataMethod: {
@@ -114,6 +116,9 @@ export default {
return this.isUpstream ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!';
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
+ },
methods: {
onClickLinkedPipeline() {
this.hideTooltips();
@@ -168,7 +173,9 @@ export default {
</div>
</div>
<div class="gl-pt-2">
- <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
+ <gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
+ {{ label }}
+ </gl-badge>
</div>
<gl-button
:id="buttonId"
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 7d333087874..40e6a01b88c 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,9 +1,9 @@
<script>
-import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
+import getPipelineDetails from 'shared_queries/pipelines/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';
+import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils';
export default {
components: {
@@ -42,8 +42,8 @@ export default {
computed: {
columnClass() {
const positionValues = {
- right: 'gl-ml-11',
- left: 'gl-mr-7',
+ right: 'gl-ml-6',
+ left: 'gl-mr-6',
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
@@ -80,8 +80,13 @@ export default {
result() {
this.loadingPipelineId = null;
},
- error() {
+ error(err, _vm, _key, type) {
this.$emit('error', LOAD_FAILURE);
+
+ reportToSentry(
+ 'linked_pipelines_column',
+ `error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`,
+ );
},
});
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
index 7d371b33220..2f1390e07d1 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
@@ -1,6 +1,7 @@
<script>
import LinkedPipeline from './linked_pipeline.vue';
import { UPSTREAM } from './constants';
+import { reportToSentry } from './utils';
export default {
components: {
@@ -42,6 +43,9 @@ export default {
return this.type === UPSTREAM;
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('linked_pipelines_column_legacy', `error: ${err}, info: ${info}`);
+ },
methods: {
onPipelineClick(downstreamNode, pipeline, index) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
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 b9bddc94ce4..65f8c231885 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -6,6 +6,7 @@ import JobGroupDropdown from './job_group_dropdown.vue';
import ActionComponent from './action_component.vue';
import { GRAPHQL } from './constants';
import { accessValue } from './accessors';
+import { reportToSentry } from './utils';
export default {
components: {
@@ -15,19 +16,28 @@ export default {
MainGraphWrapper,
},
props: {
- title: {
- type: String,
- required: true,
- },
groups: {
type: Array,
required: true,
},
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
action: {
type: Object,
required: false,
default: () => ({}),
},
+ highlightedJobs: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
jobHovered: {
type: String,
required: false,
@@ -54,6 +64,9 @@ export default {
return !isEmpty(this.action);
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
+ },
methods: {
getGroupId(group) {
return accessValue(GRAPHQL, 'groupId', group);
@@ -61,11 +74,18 @@ export default {
groupId(group) {
return `ci-badge-${escape(group.name)}`;
},
+ isFadedOut(jobName) {
+ return (
+ this.jobHovered &&
+ this.highlightedJobs.length > 1 &&
+ !this.highlightedJobs.includes(jobName)
+ );
+ },
},
};
</script>
<template>
- <main-graph-wrapper>
+ <main-graph-wrapper class="gl-px-6">
<template #stages>
<div
data-testid="stage-column-title"
@@ -90,16 +110,25 @@ export default {
:key="getGroupId(group)"
data-testid="stage-column-group"
class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ @mouseenter="$emit('jobHover', group.name)"
+ @mouseleave="$emit('jobHover', '')"
>
<job-item
v-if="group.size === 1"
:job="group.jobs[0]"
:job-hovered="jobHovered"
:pipeline-expanded="pipelineExpanded"
+ :pipeline-id="pipelineId"
css-class-job-name="gl-build-content"
+ :class="{ 'gl-opacity-3': isFadedOut(group.name) }"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
- <job-group-dropdown v-else :group="group" />
+ <job-group-dropdown
+ v-else
+ :group="group"
+ :pipeline-id="pipelineId"
+ :class="{ 'gl-opacity-3': isFadedOut(group.name) }"
+ />
</div>
</template>
</main-graph-wrapper>
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
index 258b6bf6b6d..059e8f9f8db 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
@@ -4,6 +4,7 @@ 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';
+import { reportToSentry } from './utils';
export default {
components: {
@@ -52,6 +53,9 @@ export default {
return !isEmpty(this.action);
},
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('stage_column_component_legacy', `error: ${err}, info: ${info}`);
+ },
methods: {
groupId(group) {
return `ci-badge-${escape(group.name)}`;
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
index 32588feb426..1a935599bfa 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -1,5 +1,6 @@
import Visibility from 'visibilityjs';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import * as Sentry from '~/sentry/wrapper';
import { unwrapStagesWithNeeds } from '../unwrapping_utils';
const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
@@ -9,7 +10,7 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
};
};
-const transformId = linkedPipeline => {
+const transformId = (linkedPipeline) => {
return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
};
@@ -42,7 +43,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
};
const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
- const stopStartQuery = query => {
+ const stopStartQuery = (query) => {
if (!Visibility.hidden()) {
query.startPolling(interval);
} else {
@@ -55,3 +56,10 @@ const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
};
export { unwrapPipelineData, toggleQueryPollingByVisibility };
+
+export const reportToSentry = (component, failureType) => {
+ Sentry.withScope((scope) => {
+ scope.setTag('component', component);
+ Sentry.captureException(failureType);
+ });
+};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index 35230e1511b..65c215be794 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -1,5 +1,7 @@
import * as d3 from 'd3';
-import { createUniqueLinkId } from '../../utils';
+
+export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
+
/**
* This function expects its first argument data structure
* to be the same shaped as the one generated by `parseData`,
@@ -7,21 +9,23 @@ import { createUniqueLinkId } from '../../utils';
* we find the nodes in the graph, calculate their coordinates and
* trace the lines that represent the needs of each job.
* @param {Object} nodeDict - Resulting object of `parseData` with nodes and links
- * @param {Object} jobs - An object where each key is the job name that contains the job data
- * @param {ref} svg - Reference to the svg we draw in
+ * @param {String} containerID - Id for the svg the links will be draw in
* @returns {Array} Links that contain all the information about them
*/
-export const generateLinksData = ({ links }, containerID) => {
+export const generateLinksData = ({ links }, containerID, modifier = '') => {
const containerEl = document.getElementById(containerID);
- return links.map(link => {
+ return links.map((link) => {
const path = d3.path();
const sourceId = link.source;
const targetId = link.target;
- const sourceNodeEl = document.getElementById(sourceId);
- const targetNodeEl = document.getElementById(targetId);
+ const modifiedSourceId = `${sourceId}${modifier}`;
+ const modifiedTargetId = `${targetId}${modifier}`;
+
+ const sourceNodeEl = document.getElementById(modifiedSourceId);
+ const targetNodeEl = document.getElementById(modifiedTargetId);
const sourceNodeCoordinates = sourceNodeEl.getBoundingClientRect();
const targetNodeCoordinates = targetNodeEl.getBoundingClientRect();
@@ -35,17 +39,11 @@ export const generateLinksData = ({ links }, containerID) => {
// from the total to make sure it's aligned properly. We then make the line
// positioned in the center of the job node by adding half the height
// of the job pill.
- const paddingLeft = Number(
- window
- .getComputedStyle(containerEl, null)
- .getPropertyValue('padding-left')
- .replace('px', ''),
+ const paddingLeft = parseFloat(
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'),
);
- const paddingTop = Number(
- window
- .getComputedStyle(containerEl, null)
- .getPropertyValue('padding-top')
- .replace('px', ''),
+ const paddingTop = parseFloat(
+ window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'),
);
const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft;
@@ -66,7 +64,10 @@ export const generateLinksData = ({ links }, containerID) => {
// Make cross-stages lines a straight line all the way
// until we can safely draw the bezier to look nice.
- const straightLineDestinationX = targetNodeX - 100;
+ // The adjustment number here is a magic number to make things
+ // look nice and should change if the padding changes. This goes well
+ // with gl-px-6. gl-px-8 is more like 100.
+ const straightLineDestinationX = targetNodeX - 60;
const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2;
if (straightLineDestinationX > 0) {
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
new file mode 100644
index 00000000000..89444076ae0
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -0,0 +1,140 @@
+<script>
+import { isEmpty } from 'lodash';
+import { DRAW_FAILURE } from '../../constants';
+import { createJobsHash, generateJobNeedsDict } from '../../utils';
+import { parseData } from '../parsing_utils';
+import { generateLinksData } from './drawing_utils';
+
+export default {
+ name: 'LinksInner',
+ STROKE_WIDTH: 2,
+ props: {
+ containerId: {
+ type: String,
+ required: true,
+ },
+ containerMeasurements: {
+ type: Object,
+ required: true,
+ },
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ pipelineData: {
+ type: Array,
+ required: true,
+ },
+ defaultLinkColor: {
+ type: String,
+ required: false,
+ default: 'gl-stroke-gray-200',
+ },
+ highlightedJob: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ links: [],
+ needsObject: null,
+ };
+ },
+ computed: {
+ hasHighlightedJob() {
+ return Boolean(this.highlightedJob);
+ },
+ isPipelineDataEmpty() {
+ return isEmpty(this.pipelineData);
+ },
+ highlightedJobs() {
+ // If you are hovering on a job, then the jobs we want to highlight are:
+ // The job you are currently hovering + all of its needs.
+ return this.hasHighlightedJob
+ ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]]
+ : [];
+ },
+ highlightedLinks() {
+ // If you are hovering on a job, then the links we want to highlight are:
+ // All the links whose `source` and `target` are highlighted jobs.
+ if (this.hasHighlightedJob) {
+ const filteredLinks = this.links.filter((link) => {
+ return (
+ this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
+ );
+ });
+
+ return filteredLinks.map((link) => link.ref);
+ }
+
+ return [];
+ },
+ viewBox() {
+ return [0, 0, this.containerMeasurements.width, this.containerMeasurements.height];
+ },
+ },
+ watch: {
+ highlightedJob() {
+ // On first hover, generate the needs reference
+ if (!this.needsObject) {
+ const jobs = createJobsHash(this.pipelineData);
+ this.needsObject = generateJobNeedsDict(jobs) ?? {};
+ }
+ },
+ highlightedJobs(jobs) {
+ this.$emit('highlightedJobsChange', jobs);
+ },
+ },
+ mounted() {
+ if (!isEmpty(this.pipelineData)) {
+ this.prepareLinkData();
+ }
+ },
+ methods: {
+ isLinkHighlighted(linkRef) {
+ return this.highlightedLinks.includes(linkRef);
+ },
+ prepareLinkData() {
+ try {
+ const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
+ const parsedData = parseData(arrayOfJobs);
+ this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`);
+ } catch {
+ this.$emit('error', DRAW_FAILURE);
+ }
+ },
+ getLinkClasses(link) {
+ return [
+ this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor,
+ { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
+ ];
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-relative">
+ <svg
+ id="link-svg"
+ class="gl-absolute"
+ :viewBox="viewBox"
+ :width="`${containerMeasurements.width}px`"
+ :height="`${containerMeasurements.height}px`"
+ >
+ <template>
+ <path
+ v-for="link in links"
+ :key="link.path"
+ :ref="link.ref"
+ :d="link.path"
+ class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="getLinkClasses(link)"
+ :stroke-width="$options.STROKE_WIDTH"
+ />
+ </template>
+ </svg>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
new file mode 100644
index 00000000000..0993892a574
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
+import LinksInner from './links_inner.vue';
+
+export default {
+ name: 'LinksLayer',
+ components: {
+ GlAlert,
+ LinksInner,
+ },
+ MAX_GROUPS: 200,
+ props: {
+ containerMeasurements: {
+ type: Object,
+ required: true,
+ },
+ pipelineData: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ alertDismissed: false,
+ showLinksOverride: false,
+ };
+ },
+ i18n: {
+ showLinksAnyways: __('Show links anyways'),
+ tooManyJobs: __(
+ 'This graph has a large number of jobs and showing the links between them may have performance implications.',
+ ),
+ },
+ computed: {
+ containerZero() {
+ return !this.containerMeasurements.width || !this.containerMeasurements.height;
+ },
+ numGroups() {
+ return this.pipelineData.reduce((acc, { groups }) => {
+ return acc + Number(groups.length);
+ }, 0);
+ },
+ showAlert() {
+ return !this.showLinkedLayers && !this.alertDismissed;
+ },
+ showLinkedLayers() {
+ return (
+ !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS)
+ );
+ },
+ },
+ methods: {
+ dismissAlert() {
+ this.alertDismissed = true;
+ },
+ overrideShowLinks() {
+ this.dismissAlert();
+ this.showLinksOverride = true;
+ },
+ },
+};
+</script>
+<template>
+ <links-inner
+ v-if="showLinkedLayers"
+ :container-measurements="containerMeasurements"
+ :pipeline-data="pipelineData"
+ v-bind="$attrs"
+ v-on="$listeners"
+ >
+ <slot></slot>
+ </links-inner>
+ <div v-else>
+ <gl-alert
+ v-if="showAlert"
+ class="gl-w-max-content gl-ml-4"
+ :primary-button-text="$options.i18n.showLinksAnyways"
+ @primaryAction="overrideShowLinks"
+ @dismiss="dismissAlert"
+ >
+ {{ $options.i18n.tooManyJobs }}
+ </gl-alert>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
index 1c9e3236d56..bcd7705669e 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
@@ -16,14 +16,11 @@ export default {
</script>
<template>
<div>
- <div
- class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-mb-5"
- :class="stageClasses"
- >
+ <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses">
<slot name="stages"> </slot>
</div>
<div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
:class="jobClasses"
>
<slot name="jobs"> </slot>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index af7c0d0ec3f..a20bd70e90a 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -54,7 +54,7 @@ export default {
iid: this.pipelineIid,
};
},
- update: data => data.project.pipeline,
+ update: (data) => data.project.pipeline,
error() {
this.reportFailure(LOAD_FAILURE);
},
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index 1ed415688f2..9c97fa832d0 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -33,15 +33,15 @@ import { uniqWith, isEqual } from 'lodash';
10 -> value (constant)
*/
-export const createNodeDict = nodes => {
+export const createNodeDict = (nodes) => {
return nodes.reduce((acc, node) => {
const newNode = {
...node,
- needs: node.jobs.map(job => job.needs || []).flat(),
+ needs: node.jobs.map((job) => job.needs || []).flat(),
};
if (node.size > 1) {
- node.jobs.forEach(job => {
+ node.jobs.forEach((job) => {
acc[job.name] = newNode;
});
}
@@ -54,13 +54,13 @@ export const createNodeDict = nodes => {
export const makeLinksFromNodes = (nodes, nodeDict) => {
const constantLinkValue = 10; // all links are the same weight
return nodes
- .map(group => {
- return group.jobs.map(job => {
+ .map((group) => {
+ return group.jobs.map((job) => {
if (!job.needs) {
return [];
}
- return job.needs.map(needed => {
+ return job.needs.map((needed) => {
return {
source: nodeDict[needed]?.name,
target: group.name,
@@ -74,7 +74,7 @@ export const makeLinksFromNodes = (nodes, nodeDict) => {
export const getAllAncestors = (nodes, nodeDict) => {
const needs = nodes
- .map(node => {
+ .map((node) => {
return nodeDict[node].needs || '';
})
.flat()
@@ -102,13 +102,13 @@ export const filterByAncestors = (links, nodeDict) =>
*/
const targetNode = target;
const targetNodeNeeds = nodeDict[targetNode].needs;
- const targetNodeNeedsMinusSource = targetNodeNeeds.filter(need => need !== source);
+ const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source);
const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
return !allAncestors.includes(source);
});
-export const parseData = nodes => {
+export const parseData = (nodes) => {
const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict);
const filteredLinks = filterByAncestors(allLinks, nodeDict);
@@ -121,7 +121,7 @@ export const parseData = nodes => {
The number of nodes in the most populous generation drives the height of the graph.
*/
-export const getMaxNodes = nodes => {
+export const getMaxNodes = (nodes) => {
const counts = nodes.reduce((acc, { layer }) => {
if (!acc[layer]) {
acc[layer] = 0;
@@ -141,6 +141,6 @@ export const getMaxNodes = nodes => {
to find nodes that have no relations.
*/
-export const removeOrphanNodes = sankeyfiedNodes => {
- return sankeyfiedNodes.filter(node => node.sourceLinks.length || node.targetLinks.length);
+export const removeOrphanNodes = (sankeyfiedNodes) => {
+ return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length);
};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 73e5f2542fb..8636808b69e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -1,12 +1,10 @@
<script>
-import { isEmpty } from 'lodash';
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
+import { generateLinksData } from '../graph_shared/drawing_utils';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
-import { generateLinksData } from './drawing_utils';
import { parseData } from '../parsing_utils';
-import { unwrapArrayOfJobs } from '../unwrapping_utils';
import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
import { createJobsHash, generateJobNeedsDict } from '../../utils';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
@@ -23,8 +21,6 @@ export default {
errorTexts: {
[DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
[DEFAULT]: __('An unknown error occurred.'),
- },
- warningTexts: {
[EMPTY_PIPELINE_DATA]: __(
'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.',
),
@@ -47,21 +43,24 @@ export default {
};
},
computed: {
+ hideGraph() {
+ // We won't even try to render the graph with these condition
+ // because it would cause additional errors down the line for the user
+ // which is confusing.
+ return this.isPipelineDataEmpty || this.isInvalidCiConfig;
+ },
+ pipelineStages() {
+ return this.pipelineData?.stages || [];
+ },
isPipelineDataEmpty() {
- return !this.isInvalidCiConfig && isEmpty(this.pipelineData?.stages);
+ return !this.isInvalidCiConfig && this.pipelineStages.length === 0;
},
isInvalidCiConfig() {
return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID;
},
- showAlert() {
- return this.hasError || this.hasWarning;
- },
hasError() {
return this.failureType;
},
- hasWarning() {
- return this.warning;
- },
hasHighlightedJob() {
return Boolean(this.highlightedJob);
},
@@ -73,26 +72,32 @@ export default {
return this.warning;
},
failure() {
- const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
-
- return { text, variant: 'danger', dismissible: true };
- },
- warning() {
- if (this.isPipelineDataEmpty) {
- return {
- text: this.$options.warningTexts[EMPTY_PIPELINE_DATA],
- variant: 'tip',
- dismissible: false,
- };
- } else if (this.isInvalidCiConfig) {
- return {
- text: this.$options.warningTexts[INVALID_CI_CONFIG],
- variant: 'danger',
- dismissible: false,
- };
+ switch (this.failureType) {
+ case DRAW_FAILURE:
+ return {
+ text: this.$options.errorTexts[DRAW_FAILURE],
+ variant: 'danger',
+ dismissible: true,
+ };
+ case EMPTY_PIPELINE_DATA:
+ return {
+ text: this.$options.errorTexts[EMPTY_PIPELINE_DATA],
+ variant: 'tip',
+ dismissible: false,
+ };
+ case INVALID_CI_CONFIG:
+ return {
+ text: this.$options.errorTexts[INVALID_CI_CONFIG],
+ variant: 'danger',
+ dismissible: false,
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT],
+ variant: 'danger',
+ dismissible: true,
+ };
}
-
- return null;
},
viewBox() {
return [0, 0, this.width, this.height];
@@ -100,40 +105,45 @@ export default {
highlightedJobs() {
// If you are hovering on a job, then the jobs we want to highlight are:
// The job you are currently hovering + all of its needs.
- return this.hasHighlightedJob
- ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]]
- : [];
+ return [this.highlightedJob, ...this.needsObject[this.highlightedJob]];
},
highlightedLinks() {
// If you are hovering on a job, then the links we want to highlight are:
// All the links whose `source` and `target` are highlighted jobs.
if (this.hasHighlightedJob) {
- const filteredLinks = this.links.filter(link => {
+ const filteredLinks = this.links.filter((link) => {
return (
this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
);
});
- return filteredLinks.map(link => link.ref);
+ return filteredLinks.map((link) => link.ref);
}
return [];
},
},
- mounted() {
- if (!this.isPipelineDataEmpty && !this.isInvalidCiConfig) {
- // This guarantee that all sub-elements are rendered
- // https://v3.vuejs.org/api/options-lifecycle-hooks.html#mounted
- this.$nextTick(() => {
- this.getGraphDimensions();
- this.prepareLinkData();
- });
- }
+ watch: {
+ pipelineData: {
+ immediate: true,
+ handler() {
+ if (this.isPipelineDataEmpty) {
+ this.reportFailure(EMPTY_PIPELINE_DATA);
+ } else if (this.isInvalidCiConfig) {
+ this.reportFailure(INVALID_CI_CONFIG);
+ } else {
+ this.$nextTick(() => {
+ this.computeGraphDimensions();
+ this.prepareLinkData();
+ });
+ }
+ },
+ },
},
methods: {
prepareLinkData() {
try {
- const arrayOfJobs = unwrapArrayOfJobs(this.pipelineData);
+ const arrayOfJobs = this.pipelineStages.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs);
this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID);
} catch {
@@ -141,7 +151,7 @@ export default {
}
},
getStageBackgroundClasses(index) {
- const { length } = this.pipelineData.stages;
+ const { length } = this.pipelineStages;
// It's possible for a graph to have only one stage, in which
// case we concatenate both the left and right rounding classes
if (length === 1) {
@@ -162,7 +172,7 @@ export default {
// The first time we hover, we create the object where
// we store all the data to properly highlight the needs.
if (!this.needsObject) {
- const jobs = createJobsHash(this.pipelineData);
+ const jobs = createJobsHash(this.pipelineStages);
this.needsObject = generateJobNeedsDict(jobs) ?? {};
}
@@ -171,7 +181,7 @@ export default {
removeHighlightNeeds() {
this.highlightedJob = null;
},
- getGraphDimensions() {
+ computeGraphDimensions() {
this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}`;
this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}`;
},
@@ -199,7 +209,7 @@ export default {
<template>
<div>
<gl-alert
- v-if="showAlert"
+ v-if="hasError"
:variant="alert.variant"
:dismissible="alert.dismissible"
@dismiss="alert.dismissible ? resetFailure : null"
@@ -207,7 +217,7 @@ export default {
{{ alert.text }}
</gl-alert>
<div
- v-if="!hasWarning"
+ v-if="!hideGraph"
:id="$options.CONTAINER_ID"
:ref="$options.CONTAINER_REF"
class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
@@ -227,7 +237,7 @@ export default {
</template>
</svg>
<div
- v-for="(stage, index) in pipelineData.stages"
+ v-for="(stage, index) in pipelineStages"
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index 78b69073cd3..ee26ea2f007 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -1,7 +1,25 @@
<script>
import { GlButton } from '@gitlab/ui';
+import { isExperimentEnabled } from '~/lib/utils/experimentation';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
export default {
+ i18n: {
+ control: {
+ infoMessage: s__(`Pipelines|Continuous Integration can help
+ catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver
+ code to your product environment.`),
+ buttonMessage: s__('Pipelines|Get started with Pipelines'),
+ },
+ experiment: {
+ infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build,
+ test, and deploy your code. Let GitLab take care of time
+ consuming tasks, so you can spend more time creating.`),
+ buttonMessage: s__('Pipelines|Get started with CI/CD'),
+ },
+ },
name: 'PipelinesEmptyState',
components: {
GlButton,
@@ -20,6 +38,23 @@ export default {
required: true,
},
},
+ mounted() {
+ this.track('viewed');
+ },
+ methods: {
+ track(action) {
+ if (!gon.tracking_data) {
+ return;
+ }
+
+ const { category, value, label, property } = gon.tracking_data;
+
+ Tracking.event(category, action, { value, label, property });
+ },
+ isExperimentEnabled() {
+ return isExperimentEnabled('pipelinesEmptyState');
+ },
+ },
};
</script>
<template>
@@ -29,18 +64,16 @@ export default {
</div>
<div class="col-12">
- <div class="gl-text-content">
+ <div class="text-content">
<template v-if="canSetCi">
- <h4 class="gl-text-center" data-testid="header-text">
+ <h4 data-testid="header-text" class="gl-text-center">
{{ s__('Pipelines|Build with confidence') }}
</h4>
-
<p data-testid="info-text">
{{
- s__(`Pipelines|Continuous Integration can help
- catch bugs by running your tests automatically,
- while Continuous Deployment can help you deliver
- code to your product environment.`)
+ isExperimentEnabled()
+ ? $options.i18n.experiment.infoMessage
+ : $options.i18n.control.infoMessage
}}
</p>
@@ -50,8 +83,13 @@ export default {
variant="info"
category="primary"
data-testid="get-started-pipelines"
+ @click="track('documentation_clicked')"
>
- {{ s__('Pipelines|Get started with Pipelines') }}
+ {{
+ isExperimentEnabled()
+ ? $options.i18n.experiment.buttonMessage
+ : $options.i18n.control.buttonMessage
+ }}
</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index bde0dd53aac..d1bac078642 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLink, GlPopover, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { SCHEDULE_ORIGIN } from '../../constants';
export default {
@@ -7,10 +7,16 @@ export default {
GlLink,
GlPopover,
GlSprintf,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: {
+ targetProjectFullPath: {
+ default: '',
+ },
+ },
props: {
pipeline: {
type: Object,
@@ -25,11 +31,6 @@ export default {
required: true,
},
},
- inject: {
- targetProjectFullPath: {
- default: '',
- },
- },
computed: {
user() {
return this.pipeline.user;
@@ -50,7 +51,6 @@ export default {
<div class="table-section section-10 d-none d-md-block pipeline-tags">
<gl-link
:href="pipeline.path"
- class="js-pipeline-url-link js-onboarding-pipeline-item"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
@@ -58,46 +58,49 @@ export default {
</gl-link>
<div class="label-container">
<gl-link v-if="isScheduled" :href="pipelineScheduleUrl" target="__blank">
- <span
+ <gl-badge
v-gl-tooltip
:title="__('This pipeline was triggered by a schedule.')"
- class="badge badge-info"
+ variant="info"
+ size="sm"
data-testid="pipeline-url-scheduled"
- >{{ __('Scheduled') }}</span
+ >{{ __('Scheduled') }}</gl-badge
>
</gl-link>
- <span
+ <gl-badge
v-if="pipeline.flags.latest"
v-gl-tooltip
:title="__('Latest pipeline for the most recent commit on this branch')"
- class="js-pipeline-url-latest badge badge-success"
+ variant="success"
+ size="sm"
data-testid="pipeline-url-latest"
- >{{ __('latest') }}</span
+ >{{ __('latest') }}</gl-badge
>
- <span
+ <gl-badge
v-if="pipeline.flags.yaml_errors"
v-gl-tooltip
:title="pipeline.yaml_errors"
- class="js-pipeline-url-yaml badge badge-danger"
+ variant="danger"
+ size="sm"
data-testid="pipeline-url-yaml"
- >{{ __('yaml invalid') }}</span
+ >{{ __('yaml invalid') }}</gl-badge
>
- <span
+ <gl-badge
v-if="pipeline.flags.failure_reason"
v-gl-tooltip
:title="pipeline.failure_reason"
- class="js-pipeline-url-failure badge badge-danger"
+ variant="danger"
+ size="sm"
data-testid="pipeline-url-failure"
- >{{ __('error') }}</span
+ >{{ __('error') }}</gl-badge
>
<gl-link
v-if="pipeline.flags.auto_devops"
:id="`pipeline-url-autodevops-${pipeline.id}`"
tabindex="0"
- class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
data-testid="pipeline-url-autodevops"
role="button"
- >{{ __('Auto DevOps') }}</gl-link
+ ><gl-badge variant="info" size="sm">{{ __('Auto DevOps') }}</gl-badge></gl-link
>
<gl-popover
:target="`pipeline-url-autodevops-${pipeline.id}`"
@@ -113,7 +116,7 @@ export default {
)
"
>
- <template #strong="{content}">
+ <template #strong="{ content }">
<b>{{ content }}</b>
</template>
</gl-sprintf>
@@ -123,13 +126,14 @@ export default {
__('Learn more about Auto DevOps')
}}</gl-link>
</gl-popover>
- <span
+ <gl-badge
v-if="pipeline.flags.stuck"
- class="js-pipeline-url-stuck badge badge-warning"
+ variant="warning"
+ size="sm"
data-testid="pipeline-url-stuck"
- >{{ __('stuck') }}</span
+ >{{ __('stuck') }}</gl-badge
>
- <span
+ <gl-badge
v-if="pipeline.flags.detached_merge_request_pipeline"
v-gl-tooltip
:title="
@@ -137,17 +141,19 @@ export default {
'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.',
)
"
- class="js-pipeline-url-detached badge badge-info"
+ variant="info"
+ size="sm"
data-testid="pipeline-url-detached"
- >{{ __('detached') }}</span
+ >{{ __('detached') }}</gl-badge
>
- <span
+ <gl-badge
v-if="isInFork"
v-gl-tooltip
:title="__('Pipeline ran in fork of project')"
- class="badge badge-info"
+ variant="info"
+ size="sm"
data-testid="pipeline-url-fork"
- >{{ __('fork') }}</span
+ >{{ __('fork') }}</gl-badge
>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index ff27226b408..ec7c5764be1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -246,7 +246,7 @@ export default {
filterPipelines(filters) {
this.resetRequestData();
- filters.forEach(filter => {
+ filters.forEach((filter) => {
// do not add Any for username query param, so we
// can fetch all trigger authors
if (
@@ -279,7 +279,7 @@ export default {
<div class="pipelines-container">
<div
v-if="shouldRenderTabs || shouldRenderButtons"
- class="top-area scrolling-tabs-container inner-page-scroll-tabs"
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-none"
>
<div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
<div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 55c71e299be..b13460b4c68 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -1,14 +1,19 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
- GlIcon,
- GlLink,
+ GlDropdown,
+ GlDropdownItem,
+ GlSprintf,
+ },
+ translations: {
+ artifacts: __('Artifacts'),
+ downloadArtifact: __('Download %{name} artifact'),
},
props: {
artifacts: {
@@ -19,24 +24,25 @@ export default {
};
</script>
<template>
- <div class="btn-group" role="group">
- <button
- v-gl-tooltip
- type="button"
- class="dropdown-toggle build-artifacts btn btn-default js-pipeline-dropdown-download"
- :title="__('Artifacts')"
- data-toggle="dropdown"
- :aria-label="__('Artifacts')"
+ <gl-dropdown
+ v-gl-tooltip
+ class="build-artifacts js-pipeline-dropdown-download"
+ :title="$options.translations.artifacts"
+ :text="$options.translations.artifacts"
+ :aria-label="$options.translations.artifacts"
+ icon="download"
+ text-sr-only
+ >
+ <gl-dropdown-item
+ v-for="(artifact, i) in artifacts"
+ :key="i"
+ :href="artifact.path"
+ rel="nofollow"
+ download
>
- <gl-icon name="download" />
- <gl-icon name="chevron-down" />
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="(artifact, i) in artifacts" :key="i">
- <gl-link :href="artifact.path" rel="nofollow" download
- >Download {{ artifact.name }} artifact</gl-link
- >
- </li>
- </ul>
- </div>
+ <gl-sprintf :message="$options.translations.downloadArtifact">
+ <template #name>{{ artifact.name }}</template>
+ </gl-sprintf>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index 29345f33367..127503f1307 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -33,7 +33,7 @@ export default {
},
computed: {
selectedTypes() {
- return this.value.map(i => i.type);
+ return this.value.map((i) => i.type);
},
tokens() {
return [
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
index 7224ec455f6..b6c4e617a90 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
@@ -346,7 +346,6 @@ export default {
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts"
- class="d-md-block"
/>
<gl-button
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index 581ea5fbb35..a9154d93194 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -124,7 +124,7 @@ export default {
$(
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
this.$el,
- ).on('click', e => {
+ ).on('click', (e) => {
e.stopPropagation();
});
},
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 60cb697f1af..24456574a6f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -34,10 +34,10 @@ export default {
fetchBranches(searchterm) {
Api.branches(this.config.projectId, searchterm)
.then(({ data }) => {
- this.branches = data.map(branch => branch.name);
+ this.branches = data.map((branch) => branch.name);
this.loading = false;
})
- .catch(err => {
+ .catch((err) => {
createFlash(FETCH_BRANCH_ERROR_MESSAGE);
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
index dc43d94f4fd..020a08b8cee 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
@@ -72,7 +72,7 @@ export default {
];
},
findActiveStatus() {
- return this.statuses.find(status => status.value === this.value.data);
+ return this.statuses.find((status) => status.value === this.value.data);
},
},
};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
index d6ba5fcca85..1241803c612 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
@@ -34,10 +34,10 @@ export default {
fetchTags(searchTerm) {
Api.tags(this.config.projectId, searchTerm)
.then(({ data }) => {
- this.tags = data.map(tag => tag.name);
+ this.tags = data.map((tag) => tag.name);
this.loading = false;
})
- .catch(err => {
+ .catch((err) => {
createFlash(FETCH_TAG_ERROR_MESSAGE);
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
index ae5758233bc..3db5893b565 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
@@ -45,7 +45,7 @@ export default {
return this.value.data.toLowerCase();
},
activeUser() {
- return this.users.find(user => {
+ return this.users.find((user) => {
return user.username.toLowerCase() === this.currentValue;
});
},
@@ -56,11 +56,11 @@ export default {
methods: {
fetchProjectUsers(searchTerm) {
Api.projectUsers(this.config.projectId, searchTerm)
- .then(users => {
+ .then((users) => {
this.users = users;
this.loading = false;
})
- .catch(err => {
+ .catch((err) => {
createFlash(FETCH_AUTHOR_ERROR_MESSAGE);
this.loading = false;
throw err;
@@ -80,7 +80,7 @@ export default {
v-on="$listeners"
@input="searchAuthors"
>
- <template #view="{inputValue}">
+ <template #view="{ inputValue }">
<gl-avatar
v-if="activeUser"
:size="16"
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
index aa33f622ce6..15073079c0a 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
@@ -1,22 +1,5 @@
-/**
- * This function takes the stages and add the stage name
- * at the group level as `category` to have an easier
- * implementation while constructions nodes with D3
- * @param {Array} stages
- * @returns {Array} - Array of stages with stage name at the group level as `category`
- */
-export const unwrapArrayOfJobs = (stages = []) => {
- return stages
- .map(({ name, groups }) => {
- return groups.map(group => {
- return { category: name, ...group };
- });
- })
- .flat(2);
-};
-
-const unwrapGroups = stages => {
- return stages.map(stage => {
+const unwrapGroups = (stages) => {
+ return stages.map((stage) => {
const {
groups: { nodes: groups },
} = stage;
@@ -25,21 +8,21 @@ const unwrapGroups = stages => {
};
const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
- return jobArray.map(job => {
- return { ...job, [prop]: job[prop].nodes.map(item => item[field]) };
+ return jobArray.map((job) => {
+ return { ...job, [prop]: job[prop].nodes.map((item) => item[field]) };
});
};
-const unwrapJobWithNeeds = denodedJobArray => {
+const unwrapJobWithNeeds = (denodedJobArray) => {
return unwrapNodesWithName(denodedJobArray, 'needs');
};
-const unwrapStagesWithNeeds = denodedStages => {
+const unwrapStagesWithNeeds = (denodedStages) => {
const unwrappedNestedGroups = unwrapGroups(denodedStages);
- const nodes = unwrappedNestedGroups.map(node => {
+ const nodes = unwrappedNestedGroups.map((node) => {
const { groups } = node;
- const groupsWithJobs = groups.map(group => {
+ const groupsWithJobs = groups.map((group) => {
const jobs = unwrapJobWithNeeds(group.jobs.nodes);
return { ...group, jobs };
});
diff --git a/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql b/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql
deleted file mode 100644
index 3bf6d8dc9d8..00000000000
--- a/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql
+++ /dev/null
@@ -1,17 +0,0 @@
-fragment LinkedPipelineData on Pipeline {
- id
- iid
- path
- status: detailedStatus {
- group
- label
- icon
- }
- sourceJob {
- name
- }
- project {
- name
- fullPath
- }
-}
diff --git a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql b/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql
index 1da4fa0a72b..f93908aeb04 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql
+++ b/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql
@@ -4,9 +4,23 @@ fragment PipelineStagesConnection on CiConfigStageConnection {
groups {
nodes {
name
+ size
jobs {
nodes {
name
+ script
+ beforeScript
+ afterScript
+ environment
+ allowFailure
+ tags
+ when
+ only {
+ refs
+ }
+ except {
+ refs
+ }
needs {
nodes {
name
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql
deleted file mode 100644
index 25aede49631..00000000000
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql
+++ /dev/null
@@ -1,65 +0,0 @@
-#import "../fragments/linked_pipelines.fragment.graphql"
-
-query getPipelineDetails($projectPath: ID!, $iid: ID!) {
- project(fullPath: $projectPath) {
- pipeline(iid: $iid) {
- id
- iid
- downstream {
- nodes {
- ...LinkedPipelineData
- }
- }
- upstream {
- ...LinkedPipelineData
- }
- stages {
- nodes {
- name
- status: detailedStatus {
- action {
- icon
- path
- title
- }
- }
- groups {
- nodes {
- status: detailedStatus {
- label
- group
- icon
- }
- name
- size
- jobs {
- nodes {
- name
- scheduledAt
- needs {
- nodes {
- name
- }
- }
- status: detailedStatus {
- icon
- tooltip
- hasDetails
- detailsPath
- group
- action {
- buttonTitle
- icon
- path
- title
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
index bd1b1664a1e..9f15b6c4ae3 100644
--- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
@@ -6,7 +6,7 @@ export default {
getExpandedPipelines(pipeline) {
this.mediator.service
.getPipeline(this.mediator.getExpandedParameters())
- .then(response => {
+ .then((response) => {
this.mediator.store.toggleLoading(pipeline);
this.mediator.store.storePipeline(response.data);
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index e31545bba5c..22cdb6b8f72 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -90,7 +90,7 @@ export default {
// fetch new data
return this.service
.getPipelines(this.requestData)
- .then(response => {
+ .then((response) => {
this.isLoading = false;
this.successCallback(response);
@@ -124,8 +124,8 @@ export default {
getPipelines() {
return this.service
.getPipelines(this.requestData)
- .then(response => this.successCallback(response))
- .catch(error => this.errorCallback(error));
+ .then((response) => this.successCallback(response))
+ .catch((error) => this.errorCallback(error));
},
setCommonData(pipelines) {
this.store.storePipelines(pipelines);
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 27f71d2b878..133608b9801 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -10,6 +10,7 @@ import legacyPipelineHeader from './components/legacy_header_component.vue';
import eventHub from './event_hub';
import TestReports from './components/test_reports/test_reports.vue';
import createTestReportsStore from './stores/test_reports';
+import { reportToSentry } from './components/graph/utils';
Vue.use(Translate);
@@ -20,7 +21,7 @@ const SELECTORS = {
PIPELINE_TESTS: '#js-pipeline-tests-detail',
};
-const createLegacyPipelinesDetailApp = mediator => {
+const createLegacyPipelinesDetailApp = (mediator) => {
if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
return;
}
@@ -36,6 +37,9 @@ const createLegacyPipelinesDetailApp = mediator => {
mediator,
};
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('pipeline_details_bundle_legacy_details', `error: ${err}, info: ${info}`);
+ },
render(createElement) {
return createElement('pipeline-graph-legacy', {
props: {
@@ -47,15 +51,15 @@ const createLegacyPipelinesDetailApp = mediator => {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
onResetDownstream: (parentPipeline, pipeline) =>
this.resetDownstreamPipelines(parentPipeline, pipeline),
- onClickUpstreamPipeline: pipeline => this.clickUpstreamPipeline(pipeline),
- onClickDownstreamPipeline: pipeline => this.clickDownstreamPipeline(pipeline),
+ onClickUpstreamPipeline: (pipeline) => this.clickUpstreamPipeline(pipeline),
+ onClickDownstreamPipeline: (pipeline) => this.clickDownstreamPipeline(pipeline),
},
});
},
});
};
-const createLegacyPipelineHeaderApp = mediator => {
+const createLegacyPipelineHeaderApp = (mediator) => {
if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) {
return;
}
@@ -78,6 +82,9 @@ const createLegacyPipelineHeaderApp = mediator => {
eventHub.$off('headerPostAction', this.postAction);
eventHub.$off('headerDeleteAction', this.deleteAction);
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('pipeline_details_bundle_legacy', `error: ${err}, info: ${info}`);
+ },
methods: {
postAction(path) {
this.mediator.service
@@ -125,7 +132,7 @@ const createTestDetails = () => {
});
};
-export default async function() {
+export default async function () {
createTestDetails();
createDagApp();
@@ -151,7 +158,7 @@ export default async function() {
);
const { pipelineProjectPath, pipelineIid } = dataset;
- createPipelinesDetailApp(SELECTORS.PIPELINE_DETAILS, pipelineProjectPath, pipelineIid);
+ createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, pipelineProjectPath, pipelineIid);
} catch {
Flash(__('An error occurred while loading the pipeline.'));
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js
index 1b296c305cb..2d46bb5ec26 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_graph.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { GRAPHQL } from './components/graph/constants';
+import { reportToSentry } from './components/graph/utils';
Vue.use(VueApollo);
@@ -28,6 +29,9 @@ const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid) =>
pipelineIid,
dataMethod: GRAPHQL,
},
+ errorCaptured(err, _vm, info) {
+ reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
+ },
render(createElement) {
return createElement(PipelineGraphWrapper);
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js
index 744a8272709..cba29acdb32 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_header.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_header.js
@@ -9,7 +9,7 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
-export const createPipelineHeaderApp = elSelector => {
+export const createPipelineHeaderApp = (elSelector) => {
const el = document.querySelector(elSelector);
if (!el) {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index d487970aed7..74c5fc45644 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -55,7 +55,7 @@ export default class pipelinesMediator {
return this.service
.getPipeline()
- .then(response => this.successCallback(response))
+ .then((response) => this.successCallback(response))
.catch(() => this.errorCallback())
.finally(() =>
this.poll.restart(
diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js
index c6f65277c8d..1f804a107a8 100644
--- a/app/assets/javascripts/pipelines/stores/pipeline_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js
@@ -29,11 +29,11 @@ export default class PipelineStore {
}
if (pipelineCopy.triggered && pipelineCopy.triggered.length) {
- pipelineCopy.triggered.forEach(el => {
+ pipelineCopy.triggered.forEach((el) => {
const oldPipeline =
this.state.pipeline &&
this.state.pipeline.triggered &&
- this.state.pipeline.triggered.find(element => element.id === el.id);
+ this.state.pipeline.triggered.find((element) => element.id === el.id);
this.parseTriggeredPipelines(oldPipeline, el);
});
@@ -67,8 +67,8 @@ export default class PipelineStore {
}
if (newPipeline.triggered_by?.length > 0) {
- newPipeline.triggered_by.forEach(el => {
- const oldTriggeredBy = oldPipeline.triggered_by?.find(element => element.id === el.id);
+ newPipeline.triggered_by.forEach((el) => {
+ const oldTriggeredBy = oldPipeline.triggered_by?.find((element) => element.id === el.id);
this.parseTriggeredPipelines(oldTriggeredBy, el);
});
}
@@ -88,9 +88,9 @@ export default class PipelineStore {
Vue.set(newPipeline, 'isLoading', false);
if (newPipeline.triggered && newPipeline.triggered.length > 0) {
- newPipeline.triggered.forEach(el => {
+ newPipeline.triggered.forEach((el) => {
const oldTriggered =
- oldPipeline.triggered && oldPipeline.triggered.find(element => element.id === el.id);
+ oldPipeline.triggered && oldPipeline.triggered.find((element) => element.id === el.id);
this.parseTriggeredPipelines(oldTriggered, el);
});
}
@@ -102,7 +102,7 @@ export default class PipelineStore {
* @param {Object} pipeline
*/
resetTriggeredByPipeline(parentPipeline, pipeline) {
- parentPipeline.triggered_by.forEach(el => this.closePipeline(el));
+ parentPipeline.triggered_by.forEach((el) => this.closePipeline(el));
if (pipeline.triggered_by && pipeline.triggered_by) {
this.resetTriggeredByPipeline(pipeline, pipeline.triggered_by);
@@ -129,7 +129,7 @@ export default class PipelineStore {
this.closePipeline(pipeline);
if (pipeline.triggered_by && pipeline.triggered_by.length) {
- pipeline.triggered_by.forEach(triggeredBy => this.closeTriggeredByPipeline(triggeredBy));
+ pipeline.triggered_by.forEach((triggeredBy) => this.closeTriggeredByPipeline(triggeredBy));
}
}
@@ -139,10 +139,10 @@ export default class PipelineStore {
* @param {Object} pipeline
*/
resetTriggeredPipelines(parentPipeline, pipeline) {
- parentPipeline.triggered.forEach(el => this.closePipeline(el));
+ parentPipeline.triggered.forEach((el) => this.closePipeline(el));
if (pipeline.triggered && pipeline.triggered.length) {
- pipeline.triggered.forEach(el => this.resetTriggeredPipelines(pipeline, el));
+ pipeline.triggered.forEach((el) => this.resetTriggeredPipelines(pipeline, el));
}
}
@@ -165,7 +165,7 @@ export default class PipelineStore {
this.closePipeline(pipeline);
if (pipeline.triggered && pipeline.triggered.length) {
- pipeline.triggered.forEach(triggered => this.closeTriggeredPipeline(triggered));
+ pipeline.triggered.forEach((triggered) => this.closeTriggeredPipeline(triggered));
}
}
@@ -198,6 +198,9 @@ export default class PipelineStore {
}
removeExpandedPipelineToRequestData(id) {
- this.state.expandedPipelines.splice(this.state.expandedPipelines.findIndex(el => el === id), 1);
+ this.state.expandedPipelines.splice(
+ this.state.expandedPipelines.findIndex((el) => el === id),
+ 1,
+ );
}
}
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index 56f769c00fa..c31e7dd114f 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -1,18 +1,18 @@
import { addIconStatus, formattedTime } from './utils';
-export const getTestSuites = state => {
+export const getTestSuites = (state) => {
const { test_suites: testSuites = [] } = state.testReports;
- return testSuites.map(suite => ({
+ return testSuites.map((suite) => ({
...suite,
formattedTime: formattedTime(suite.total_time),
}));
};
-export const getSelectedSuite = state =>
+export const getSelectedSuite = (state) =>
state.testReports?.test_suites?.[state.selectedSuiteIndex] || {};
-export const getSuiteTests = state => {
+export const getSuiteTests = (state) => {
const { test_cases: testCases = [] } = getSelectedSuite(state);
const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage;
@@ -20,4 +20,4 @@ export const getSuiteTests = state => {
return testCases.map(addIconStatus).slice(start, start + perPage);
};
-export const getSuiteTestCount = state => getSelectedSuite(state)?.test_cases?.length || 0;
+export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js
index 88f61b09025..204dfc2fb01 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/index.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js
@@ -7,7 +7,7 @@ import mutations from './mutations';
Vue.use(Vuex);
-export default initialState =>
+export default (initialState) =>
new Vuex.Store({
actions,
getters,
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
index 42406e5a67a..5c1f27b166a 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -25,7 +25,7 @@ export const formattedTime = (seconds = 0) => {
return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) });
};
-export const addIconStatus = testCase => ({
+export const addIconStatus = (testCase) => ({
...testCase,
icon: iconForTestStatus(testCase.status),
formattedTime: formattedTime(testCase.execution_time),
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 28d6c0edb0f..50bb23b7e63 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,12 +1,11 @@
import { pickBy } from 'lodash';
import { SUPPORTED_FILTER_PARAMETERS } from './constants';
+import { createNodeDict } from './components/parsing_utils';
-export const validateParams = params => {
+export const validateParams = (params) => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
-export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
-
/**
* This function takes the stages array and transform it
* into a hash where each key is a job name and the job data
@@ -15,19 +14,8 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam
* @returns {Object} - Hash of jobs
*/
export const createJobsHash = (stages = []) => {
- const jobsHash = {};
-
- stages.forEach(stage => {
- if (stage.groups.length > 0) {
- stage.groups.forEach(group => {
- group.jobs.forEach(job => {
- jobsHash[job.name] = job;
- });
- });
- }
- });
-
- return jobsHash;
+ const nodes = stages.flatMap(({ groups }) => groups);
+ return createNodeDict(nodes);
};
/**
@@ -44,18 +32,26 @@ export const generateJobNeedsDict = (jobs = {}) => {
const arrOfJobNames = Object.keys(jobs);
return arrOfJobNames.reduce((acc, value) => {
- const recursiveNeeds = jobName => {
+ const recursiveNeeds = (jobName) => {
if (!jobs[jobName]?.needs) {
return [];
}
return jobs[jobName].needs
- .map(job => {
+ .map((job) => {
// If we already have the needs of a job in the accumulator,
// then we use the memoized data instead of the recursive call
// to save some performance.
const newNeeds = acc[job] ?? recursiveNeeds(job);
+ // In case it's a parallel job (size > 1), the name of the group
+ // and the job will be different. This mean we also need to add the group name
+ // to the list of `needs` to ensure we can properly reference it.
+ const group = jobs[job];
+ if (group.size > 1) {
+ return [job, group.name, ...newNeeds];
+ }
+
return [job, ...newNeeds];
})
.flat(Infinity);