diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipeline_graph')
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" /> |