Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipeline_graph')
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js10
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue76
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue109
4 files changed, 78 insertions, 125 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js b/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
index 45940d4a39c..35230e1511b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
@@ -1,5 +1,5 @@
import * as d3 from 'd3';
-import { createUniqueJobId } from '../../utils';
+import { createUniqueLinkId } from '../../utils';
/**
* This function expects its first argument data structure
* to be the same shaped as the one generated by `parseData`,
@@ -12,13 +12,13 @@ import { createUniqueJobId } from '../../utils';
* @returns {Array} Links that contain all the information about them
*/
-export const generateLinksData = ({ links }, jobs, containerID) => {
+export const generateLinksData = ({ links }, containerID) => {
const containerEl = document.getElementById(containerID);
return links.map(link => {
const path = d3.path();
- const sourceId = jobs[link.source].id;
- const targetId = jobs[link.target].id;
+ const sourceId = link.source;
+ const targetId = link.target;
const sourceNodeEl = document.getElementById(sourceId);
const targetNodeEl = document.getElementById(targetId);
@@ -89,7 +89,7 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
...link,
source: sourceId,
target: targetId,
- ref: createUniqueJobId(sourceId, targetId),
+ ref: createUniqueLinkId(sourceId, targetId),
path: path.toString(),
};
});
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue
deleted file mode 100644
index 3cc76425e2a..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<script>
-import { GlTab, GlTabs } from '@gitlab/ui';
-import jsYaml from 'js-yaml';
-import PipelineGraph from './pipeline_graph.vue';
-import { preparePipelineGraphData } from '../../utils';
-
-export default {
- FILE_CONTENT_SELECTOR: '#blob-content',
- EMPTY_FILE_SELECTOR: '.nothing-here-block',
-
- components: {
- GlTab,
- GlTabs,
- PipelineGraph,
- },
- props: {
- blobData: {
- required: true,
- type: String,
- },
- },
- data() {
- return {
- selectedTabIndex: 0,
- pipelineData: {},
- };
- },
- computed: {
- isVisualizationTab() {
- return this.selectedTabIndex === 1;
- },
- },
- async created() {
- if (this.blobData) {
- // The blobData in this case represents the gitlab-ci.yml data
- const json = await jsYaml.load(this.blobData);
- this.pipelineData = preparePipelineGraphData(json);
- }
- },
- methods: {
- // This is used because the blob page still uses haml, and we can't make
- // our haml hide the unused section so we resort to a standard query here.
- toggleFileContent({ isFileTab }) {
- const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR);
- const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR);
-
- const elementToHide = el || emptySection;
-
- if (!elementToHide) {
- return;
- }
-
- // Checking for the current style display prevents user
- // from toggling visiblity on and off when clicking on the tab
- if (!isFileTab && elementToHide.style.display !== 'none') {
- elementToHide.style.display = 'none';
- }
-
- if (isFileTab && elementToHide.style.display === 'none') {
- elementToHide.style.display = 'block';
- }
- },
- },
-};
-</script>
-<template>
- <div>
- <div>
- <gl-tabs v-model="selectedTabIndex">
- <gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" />
- <gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" />
- </gl-tabs>
- </div>
- <pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
index a0c35f54c0e..51a95612d3f 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -10,10 +10,6 @@ export default {
type: String,
required: true,
},
- jobId: {
- type: String,
- required: true,
- },
isHighlighted: {
type: Boolean,
required: false,
@@ -45,7 +41,7 @@ export default {
},
methods: {
onMouseEnter() {
- this.$emit('on-mouse-enter', this.jobId);
+ this.$emit('on-mouse-enter', this.jobName);
},
onMouseLeave() {
this.$emit('on-mouse-leave');
@@ -56,7 +52,7 @@ export default {
<template>
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
- :id="jobId"
+ :id="jobName"
class="gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
:class="jobPillClasses"
@mouseover="onMouseEnter"
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 11ad2f2a3b6..73e5f2542fb 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -6,8 +6,10 @@ 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';
+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';
export default {
components: {
@@ -22,6 +24,12 @@ export default {
[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.',
+ ),
+ [INVALID_CI_CONFIG]: __('Your CI configuration file is invalid.'),
+ },
props: {
pipelineData: {
required: true,
@@ -40,18 +48,51 @@ export default {
},
computed: {
isPipelineDataEmpty() {
- return isEmpty(this.pipelineData);
+ return !this.isInvalidCiConfig && isEmpty(this.pipelineData?.stages);
+ },
+ 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);
},
+ alert() {
+ if (this.hasError) {
+ return this.failure;
+ }
+
+ return this.warning;
+ },
failure() {
const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
- return { text, variant: 'danger' };
+ 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,
+ };
+ }
+
+ return null;
},
viewBox() {
return [0, 0, this.width, this.height];
@@ -80,19 +121,21 @@ export default {
},
},
mounted() {
- if (!this.isPipelineDataEmpty) {
- this.getGraphDimensions();
- this.drawJobLinks();
+ 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();
+ });
}
},
methods: {
- drawJobLinks() {
- const { stages, jobs } = this.pipelineData;
- const unwrappedGroups = this.unwrapPipelineData(stages);
-
+ prepareLinkData() {
try {
- const parsedData = parseData(unwrappedGroups);
- this.links = generateLinksData(parsedData, jobs, this.$options.CONTAINER_ID);
+ const arrayOfJobs = unwrapArrayOfJobs(this.pipelineData);
+ const parsedData = parseData(arrayOfJobs);
+ this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID);
} catch {
this.reportFailure(DRAW_FAILURE);
}
@@ -119,7 +162,8 @@ 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) {
- this.needsObject = generateJobNeedsDict(this.pipelineData) ?? {};
+ const jobs = createJobsHash(this.pipelineData);
+ this.needsObject = generateJobNeedsDict(jobs) ?? {};
}
this.highlightedJob = uniqueJobId;
@@ -127,18 +171,9 @@ export default {
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`;
+ this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}`;
+ this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}`;
},
reportFailure(errorType) {
this.failureType = errorType;
@@ -163,21 +198,20 @@ export default {
</script>
<template>
<div>
- <gl-alert v-if="hasError" :variant="failure.variant" @dismiss="resetFailure">
- {{ failure.text }}
- </gl-alert>
- <gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
- {{
- __(
- 'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.',
- )
- }}
+ <gl-alert
+ v-if="showAlert"
+ :variant="alert.variant"
+ :dismissible="alert.dismissible"
+ @dismiss="alert.dismissible ? resetFailure : null"
+ >
+ {{ alert.text }}
</gl-alert>
<div
- v-else
+ v-if="!hasWarning"
: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"
+ data-testid="graph-container"
>
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
<template>
@@ -210,10 +244,9 @@ export default {
<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)"
+ :is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
+ :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
@on-mouse-enter="highlightNeeds"
@on-mouse-leave="removeHighlightNeeds"
/>