diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-14 15:10:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-14 15:10:54 +0300 |
commit | 18873553de98259d0558157f78198b38ddd02b31 (patch) | |
tree | cbdf0261e8a72975b7044fe7df8a1439c93ed4d5 /app/assets/javascripts/pipelines | |
parent | 0ea7b5c8a3f7afaae6b03279af56cd880d538bd7 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/pipelines')
-rw-r--r-- | app/assets/javascripts/pipelines/components/graph/graph_component.vue | 108 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue | 10 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/graph/job_item.vue | 9 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/graph/stage_column_component.vue | 9 | ||||
-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) | 24 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue | 137 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue | 86 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue | 2 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/pipeline_details_bundle.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/utils.js | 2 |
10 files changed, 338 insertions, 51 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 5958d198be2..96a674e342f 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,5 +1,6 @@ <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'; @@ -8,6 +9,7 @@ import { reportToSentry } from './utils'; export default { name: 'PipelineGraph', components: { + LinksLayer, LinkedGraphWrapper, LinkedPipelinesColumn, StageColumnComponent, @@ -32,9 +34,15 @@ export default { DOWNSTREAM, UPSTREAM, }, + CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF', + BASE_CONTAINER_ID: 'pipeline-links-container', data() { return { hoveredJobName: '', + measurements: { + width: 0, + height: 0, + }, pipelineExpanded: { jobName: '', expanded: false, @@ -42,6 +50,9 @@ export default { }; }, computed: { + containerId() { + return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`; + }, downstreamPipelines() { return this.hasDownstreamPipelines ? this.pipeline.downstream : []; }, @@ -54,12 +65,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 @@ -72,7 +84,19 @@ export default { 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; }, @@ -88,43 +112,57 @@ export default { <template> <div class="js-pipeline-graph"> <div + :id="containerId" + :ref="containerId" 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 }" > - <linked-graph-wrapper> - <template #upstream> - <linked-pipelines-column - v-if="showUpstreamPipelines" - :linked-pipelines="upstreamPipelines" - :column-title="__('Upstream')" - :type="$options.pipelineTypeConstants.UPSTREAM" - @error="emit('error', errorType)" - /> - </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')" - /> - </template> - <template #downstream> - <linked-pipelines-column - v-if="showDownstreamPipelines" - :linked-pipelines="downstreamPipelines" - :column-title="__('Downstream')" - :type="$options.pipelineTypeConstants.DOWNSTREAM" - @downstreamHovered="setJob" - @pipelineExpandToggle="togglePipelineExpanded" - @error="emit('error', errorType)" - /> - </template> - </linked-graph-wrapper> + <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" + > + <linked-graph-wrapper> + <template #upstream> + <linked-pipelines-column + v-if="showUpstreamPipelines" + :linked-pipelines="upstreamPipelines" + :column-title="__('Upstream')" + :type="$options.pipelineTypeConstants.UPSTREAM" + @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" + :pipeline-id="pipeline.id" + @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @jobHover="setJob" + /> + </template> + <template #downstream> + <linked-pipelines-column + v-if="showDownstreamPipelines" + :linked-pipelines="downstreamPipelines" + :column-title="__('Downstream')" + :type="$options.pipelineTypeConstants.DOWNSTREAM" + @downstreamHovered="setJob" + @pipelineExpandToggle="togglePipelineExpanded" + @error="onError" + /> + </template> + </linked-graph-wrapper> + </links-layer> </div> </div> </template> 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 b6e756ceaba..08d6162aeb8 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -23,8 +23,16 @@ 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}`; @@ -41,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 4f414cbb31f..8262d728a24 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -74,6 +74,11 @@ export default { required: false, default: () => ({}), }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { boundary() { @@ -85,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 : {}; }, @@ -146,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" > 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 45d183cce97..e65ae318952 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -24,6 +24,10 @@ export default { type: Array, required: true, }, + pipelineId: { + type: Number, + required: true, + }, action: { type: Object, required: false, @@ -94,16 +98,19 @@ 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" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> - <job-group-dropdown v-else :group="group" /> + <job-group-dropdown v-else :group="group" :pipeline-id="pipelineId" /> </div> </template> </main-graph-wrapper> 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 b550c599fda..fe59af12011 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,12 +9,11 @@ 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) => { const path = d3.path(); @@ -20,8 +21,11 @@ export const generateLinksData = ({ links }, containerID) => { 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,11 +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; 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..480cb032e11 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -0,0 +1,137 @@ +<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) ?? {}; + } + }, + }, + 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/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue index 6c957d09e46..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,9 +1,9 @@ <script> 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 { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants'; import { createJobsHash, generateJobNeedsDict } from '../../utils'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 8c9040dce04..133608b9801 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -158,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/utils.js b/app/assets/javascripts/pipelines/utils.js index 54fcd5502de..50bb23b7e63 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -6,8 +6,6 @@ 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 |