237 lines
6.6 KiB
Vue
237 lines
6.6 KiB
Vue
<script>
|
|
import { GlAlert, GlButton, GlEmptyState, GlSprintf } from '@gitlab/ui';
|
|
import { isEmpty } from 'lodash';
|
|
import { __ } from '~/locale';
|
|
import { fetchPolicies } from '~/lib/graphql';
|
|
import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
|
|
import DagGraph from './dag_graph.vue';
|
|
import DagAnnotations from './dag_annotations.vue';
|
|
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
|
|
import { parseData } from '../parsing_utils';
|
|
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
|
|
|
|
export default {
|
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
|
name: 'Dag',
|
|
components: {
|
|
DagAnnotations,
|
|
DagGraph,
|
|
GlAlert,
|
|
GlSprintf,
|
|
GlEmptyState,
|
|
GlButton,
|
|
},
|
|
inject: {
|
|
dagDocPath: {
|
|
default: null,
|
|
},
|
|
emptySvgPath: {
|
|
default: '',
|
|
},
|
|
pipelineIid: {
|
|
default: '',
|
|
},
|
|
pipelineProjectPath: {
|
|
default: '',
|
|
},
|
|
},
|
|
apollo: {
|
|
graphData: {
|
|
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
|
|
query: getDagVisData,
|
|
variables() {
|
|
return {
|
|
projectPath: this.pipelineProjectPath,
|
|
iid: this.pipelineIid,
|
|
};
|
|
},
|
|
update(data) {
|
|
const {
|
|
stages: { nodes: stages },
|
|
} = data.project.pipeline;
|
|
|
|
const unwrappedGroups = stages
|
|
.map(({ name, groups: { nodes: groups } }) => {
|
|
return groups.map(group => {
|
|
return { category: name, ...group };
|
|
});
|
|
})
|
|
.flat(2);
|
|
|
|
const nodes = unwrappedGroups.map(group => {
|
|
const jobs = group.jobs.nodes.map(({ name, needs }) => {
|
|
return { name, needs: needs.nodes.map(need => need.name) };
|
|
});
|
|
|
|
return { ...group, jobs };
|
|
});
|
|
|
|
return nodes;
|
|
},
|
|
error() {
|
|
this.reportFailure(LOAD_FAILURE);
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
annotationsMap: {},
|
|
failureType: null,
|
|
graphData: null,
|
|
showFailureAlert: false,
|
|
hasNoDependentJobs: false,
|
|
};
|
|
},
|
|
errorTexts: {
|
|
[LOAD_FAILURE]: __('We are currently unable to fetch data for this graph.'),
|
|
[PARSE_FAILURE]: __('There was an error parsing the data for this graph.'),
|
|
[UNSUPPORTED_DATA]: __('DAG visualization requires at least 3 dependent jobs.'),
|
|
[DEFAULT]: __('An unknown error occurred while loading this graph.'),
|
|
},
|
|
emptyStateTexts: {
|
|
title: __('Start using Directed Acyclic Graphs (DAG)'),
|
|
firstDescription: __(
|
|
"This pipeline does not use the %{codeStart}needs%{codeEnd} keyword and can't be represented as a directed acyclic graph.",
|
|
),
|
|
secondDescription: __(
|
|
'Using %{codeStart}needs%{codeEnd} allows jobs to run before their stage is reached, as soon as their individual dependencies are met, which speeds up your pipelines.',
|
|
),
|
|
button: __('Learn more about job dependencies'),
|
|
},
|
|
computed: {
|
|
failure() {
|
|
switch (this.failureType) {
|
|
case LOAD_FAILURE:
|
|
return {
|
|
text: this.$options.errorTexts[LOAD_FAILURE],
|
|
variant: 'danger',
|
|
};
|
|
case PARSE_FAILURE:
|
|
return {
|
|
text: this.$options.errorTexts[PARSE_FAILURE],
|
|
variant: 'danger',
|
|
};
|
|
case UNSUPPORTED_DATA:
|
|
return {
|
|
text: this.$options.errorTexts[UNSUPPORTED_DATA],
|
|
variant: 'info',
|
|
};
|
|
default:
|
|
return {
|
|
text: this.$options.errorTexts[DEFAULT],
|
|
variant: 'danger',
|
|
};
|
|
}
|
|
},
|
|
processedData() {
|
|
return this.processGraphData(this.graphData);
|
|
},
|
|
shouldDisplayAnnotations() {
|
|
return !isEmpty(this.annotationsMap);
|
|
},
|
|
shouldDisplayGraph() {
|
|
return Boolean(!this.showFailureAlert && !this.hasNoDependentJobs && this.graphData);
|
|
},
|
|
},
|
|
methods: {
|
|
addAnnotationToMap({ uid, source, target }) {
|
|
this.$set(this.annotationsMap, uid, { source, target });
|
|
},
|
|
processGraphData(data) {
|
|
let parsed;
|
|
|
|
try {
|
|
parsed = parseData(data);
|
|
} catch {
|
|
this.reportFailure(PARSE_FAILURE);
|
|
return {};
|
|
}
|
|
|
|
if (parsed.links.length === 1) {
|
|
this.reportFailure(UNSUPPORTED_DATA);
|
|
return {};
|
|
}
|
|
|
|
// If there are no links, we don't report failure
|
|
// as it simply means the user does not use job dependencies
|
|
if (parsed.links.length === 0) {
|
|
this.hasNoDependentJobs = true;
|
|
return {};
|
|
}
|
|
|
|
return parsed;
|
|
},
|
|
hideAlert() {
|
|
this.showFailureAlert = false;
|
|
},
|
|
removeAnnotationFromMap({ uid }) {
|
|
this.$delete(this.annotationsMap, uid);
|
|
},
|
|
reportFailure(type) {
|
|
this.showFailureAlert = true;
|
|
this.failureType = type;
|
|
},
|
|
updateAnnotation({ type, data }) {
|
|
switch (type) {
|
|
case ADD_NOTE:
|
|
this.addAnnotationToMap(data);
|
|
break;
|
|
case REMOVE_NOTE:
|
|
this.removeAnnotationFromMap(data);
|
|
break;
|
|
case REPLACE_NOTES:
|
|
this.annotationsMap = data;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<template>
|
|
<div>
|
|
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">
|
|
{{ failure.text }}
|
|
</gl-alert>
|
|
|
|
<div class="gl-relative">
|
|
<dag-annotations v-if="shouldDisplayAnnotations" :annotations="annotationsMap" />
|
|
<dag-graph
|
|
v-if="shouldDisplayGraph"
|
|
:graph-data="processedData"
|
|
@onFailure="reportFailure"
|
|
@update-annotation="updateAnnotation"
|
|
/>
|
|
<gl-empty-state
|
|
v-else-if="hasNoDependentJobs"
|
|
:svg-path="emptySvgPath"
|
|
:title="$options.emptyStateTexts.title"
|
|
>
|
|
<template #description>
|
|
<div class="gl-text-left">
|
|
<p>
|
|
<gl-sprintf :message="$options.emptyStateTexts.firstDescription">
|
|
<template #code="{ content }">
|
|
<code>{{ content }}</code>
|
|
</template>
|
|
</gl-sprintf>
|
|
</p>
|
|
<p>
|
|
<gl-sprintf :message="$options.emptyStateTexts.secondDescription">
|
|
<template #code="{ content }">
|
|
<code>{{ content }}</code>
|
|
</template>
|
|
</gl-sprintf>
|
|
</p>
|
|
</div>
|
|
</template>
|
|
<template v-if="dagDocPath" #actions>
|
|
<gl-button :href="dagDocPath" target="__blank" variant="success">
|
|
{{ $options.emptyStateTexts.button }}
|
|
</gl-button>
|
|
</template>
|
|
</gl-empty-state>
|
|
</div>
|
|
</div>
|
|
</template>
|