summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue')
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue177
1 files changed, 167 insertions, 10 deletions
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 6a0d3cce1f3..3a2b8a20bae 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -1,8 +1,13 @@
<script>
import { isEmpty } from 'lodash';
import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
+import { generateLinksData } from './drawing_utils';
+import { parseData } from '../parsing_utils';
+import { DRAW_FAILURE, DEFAULT } from '../../constants';
+import { generateJobNeedsDict } from '../../utils';
export default {
components: {
@@ -10,28 +15,174 @@ export default {
JobPill,
StagePill,
},
+ CONTAINER_REF: 'PIPELINE_GRAPH_CONTAINER_REF',
+ CONTAINER_ID: 'pipeline-graph-container',
+ STROKE_WIDTH: 2,
+ errorTexts: {
+ [DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
+ [DEFAULT]: __('An unknown error occurred.'),
+ },
props: {
pipelineData: {
required: true,
type: Object,
},
},
+ data() {
+ return {
+ failureType: null,
+ highlightedJob: null,
+ links: [],
+ needsObject: null,
+ height: 0,
+ width: 0,
+ };
+ },
computed: {
isPipelineDataEmpty() {
return isEmpty(this.pipelineData);
},
- emptyClass() {
- return !this.isPipelineDataEmpty ? 'gl-py-7' : '';
+ hasError() {
+ return this.failureType;
+ },
+ hasHighlightedJob() {
+ return Boolean(this.highlightedJob);
+ },
+ failure() {
+ const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
+
+ return { text, variant: 'danger' };
+ },
+ viewBox() {
+ return [0, 0, this.width, this.height];
+ },
+ 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 [];
+ },
+ },
+ mounted() {
+ if (!this.isPipelineDataEmpty) {
+ this.getGraphDimensions();
+ this.drawJobLinks();
+ }
+ },
+ methods: {
+ drawJobLinks() {
+ const { stages, jobs } = this.pipelineData;
+ const unwrappedGroups = this.unwrapPipelineData(stages);
+
+ try {
+ const parsedData = parseData(unwrappedGroups);
+ this.links = generateLinksData(parsedData, jobs, this.$options.CONTAINER_ID);
+ } catch {
+ this.reportFailure(DRAW_FAILURE);
+ }
+ },
+ getStageBackgroundClass(index) {
+ const { length } = this.pipelineData.stages;
+
+ if (length === 1) {
+ return 'stage-rounded';
+ } else if (index === 0) {
+ return 'stage-left-rounded';
+ } else if (index === length - 1) {
+ return 'stage-right-rounded';
+ }
+
+ return '';
+ },
+ highlightNeeds(uniqueJobId) {
+ // The first time we hover, we create the object where
+ // we store all the data to properly highlight the needs.
+ if (!this.needsObject) {
+ this.needsObject = generateJobNeedsDict(this.pipelineData) ?? {};
+ }
+
+ this.highlightedJob = uniqueJobId;
+ },
+ removeHighlightNeeds() {
+ this.highlightedJob = null;
+ },
+ unwrapPipelineData(stages) {
+ return stages
+ .map(({ name, groups }) => {
+ return groups.map(group => {
+ return { category: name, ...group };
+ });
+ })
+ .flat(2);
+ },
+ getGraphDimensions() {
+ this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}px`;
+ this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}px`;
+ },
+ reportFailure(errorType) {
+ this.failureType = errorType;
+ },
+ resetFailure() {
+ this.failureType = null;
+ },
+ isJobHighlighted(jobName) {
+ return this.highlightedJobs.includes(jobName);
+ },
+ isLinkHighlighted(linkRef) {
+ return this.highlightedLinks.includes(linkRef);
+ },
+ getLinkClasses(link) {
+ return [
+ this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : 'gl-stroke-gray-200',
+ { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
+ ];
},
},
};
</script>
<template>
- <div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass">
+ <div>
+ <gl-alert v-if="hasError" :variant="failure.variant" @dismiss="resetFailure">
+ {{ failure.text }}
+ </gl-alert>
<gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
{{ __('No content to show') }}
</gl-alert>
- <template v-else>
+ <div
+ v-else
+ :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"
+ >
+ <svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
+ <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>
<div
v-for="(stage, index) in pipelineData.stages"
:key="`${stage.name}-${index}`"
@@ -39,19 +190,25 @@ export default {
>
<div
class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
- :class="{
- 'stage-left-rounded': index === 0,
- 'stage-right-rounded': index === pipelineData.stages.length - 1,
- }"
+ :class="getStageBackgroundClass(index)"
>
<stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
</div>
<div
class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
>
- <job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" />
+ <job-pill
+ v-for="group in stage.groups"
+ :key="group.name"
+ :job-id="group.id"
+ :job-name="group.name"
+ :is-highlighted="hasHighlightedJob && isJobHighlighted(group.id)"
+ :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.id)"
+ @on-mouse-enter="highlightNeeds"
+ @on-mouse-leave="removeHighlightNeeds"
+ />
</div>
</div>
- </template>
+ </div>
</div>
</template>