Commit dc3d354a authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Sarah Groff Hennigh-Palermo

Resolve "Apply Links Layer to Pipeline Editing Vis"

parent 2927d330
......@@ -10,6 +10,10 @@ export default {
type: String,
required: true,
},
pipelineId: {
type: Number,
required: true,
},
isHighlighted: {
type: Boolean,
required: false,
......@@ -32,6 +36,9 @@ export default {
},
},
computed: {
id() {
return `${this.jobName}-${this.pipelineId}`;
},
jobPillClasses() {
return [
{ 'gl-opacity-3': this.isFadedOut },
......@@ -52,7 +59,7 @@ export default {
<template>
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
:id="jobName"
:id="id"
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"
......
......@@ -3,9 +3,7 @@ import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
import { createJobsHash, generateJobNeedsDict } from '../../utils';
import { generateLinksData } from '../graph_shared/drawing_utils';
import { parseData } from '../parsing_utils';
import LinksLayer from '../graph_shared/links_layer.vue';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
......@@ -13,10 +11,12 @@ export default {
components: {
GlAlert,
JobPill,
LinksLayer,
StagePill,
},
CONTAINER_REF: 'PIPELINE_GRAPH_CONTAINER_REF',
CONTAINER_ID: 'pipeline-graph-container',
BASE_CONTAINER_ID: 'pipeline-graph-container',
PIPELINE_ID: 0,
STROKE_WIDTH: 2,
errorTexts: {
[DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
......@@ -36,33 +36,16 @@ export default {
return {
failureType: null,
highlightedJob: null,
links: [],
needsObject: null,
height: 0,
width: 0,
highlightedJobs: [],
measurements: {
height: 0,
width: 0,
},
};
},
computed: {
hideGraph() {
// We won't even try to render the graph with these condition
// because it would cause additional errors down the line for the user
// which is confusing.
return this.isPipelineDataEmpty || this.isInvalidCiConfig;
},
pipelineStages() {
return this.pipelineData?.stages || [];
},
isPipelineDataEmpty() {
return !this.isInvalidCiConfig && this.pipelineStages.length === 0;
},
isInvalidCiConfig() {
return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID;
},
hasError() {
return this.failureType;
},
hasHighlightedJob() {
return Boolean(this.highlightedJob);
containerId() {
return `${this.$options.BASE_CONTAINER_ID}-${this.$options.PIPELINE_ID}`;
},
failure() {
switch (this.failureType) {
......@@ -92,28 +75,26 @@ export default {
};
}
},
viewBox() {
return [0, 0, this.width, this.height];
hasError() {
return this.failureType;
},
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.highlightedJob, ...this.needsObject[this.highlightedJob]];
hasHighlightedJob() {
return Boolean(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 [];
hideGraph() {
// We won't even try to render the graph with these condition
// because it would cause additional errors down the line for the user
// which is confusing.
return this.isPipelineDataEmpty || this.isInvalidCiConfig;
},
isInvalidCiConfig() {
return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID;
},
isPipelineDataEmpty() {
return !this.isInvalidCiConfig && this.pipelineStages.length === 0;
},
pipelineStages() {
return this.pipelineData?.stages || [];
},
},
watch: {
......@@ -127,21 +108,17 @@ export default {
} else {
this.$nextTick(() => {
this.computeGraphDimensions();
this.prepareLinkData();
});
}
},
},
},
methods: {
prepareLinkData() {
try {
const arrayOfJobs = this.pipelineStages.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs);
this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID);
} catch {
this.reportFailure(DRAW_FAILURE);
}
computeGraphDimensions() {
this.measurements = {
width: this.$refs[this.$options.CONTAINER_REF].scrollWidth,
height: this.$refs[this.$options.CONTAINER_REF].scrollHeight,
};
},
getStageBackgroundClasses(index) {
const { length } = this.pipelineStages;
......@@ -161,22 +138,14 @@ export default {
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) {
const jobs = createJobsHash(this.pipelineStages);
this.needsObject = generateJobNeedsDict(jobs) ?? {};
}
this.highlightedJob = uniqueJobId;
isJobHighlighted(jobName) {
return this.highlightedJobs.includes(jobName);
},
removeHighlightNeeds() {
this.highlightedJob = null;
onError(error) {
this.reportFailure(error.type);
},
computeGraphDimensions() {
this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}`;
this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}`;
removeHoveredJob() {
this.highlightedJob = null;
},
reportFailure(errorType) {
this.failureType = errorType;
......@@ -184,17 +153,11 @@ export default {
resetFailure() {
this.failureType = null;
},
isJobHighlighted(jobName) {
return this.highlightedJobs.includes(jobName);
},
isLinkHighlighted(linkRef) {
return this.highlightedLinks.includes(linkRef);
setHoveredJob(jobName) {
this.highlightedJob = jobName;
},
getLinkClasses(link) {
return [
this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : 'gl-stroke-gray-200',
{ 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
];
updateHighlightedJobs(jobs) {
this.highlightedJobs = jobs;
},
},
};
......@@ -211,48 +174,47 @@ export default {
</gl-alert>
<div
v-if="!hideGraph"
:id="$options.CONTAINER_ID"
:id="containerId"
: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">
<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"
/>
</svg>
<div
v-for="(stage, index) in pipelineStages"
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
<links-layer
:pipeline-data="pipelineStages"
:pipeline-id="$options.PIPELINE_ID"
:container-id="containerId"
:container-measurements="measurements"
:highlighted-job="highlightedJob"
@highlightedJobsChange="updateHighlightedJobs"
@error="onError"
>
<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="getStageBackgroundClasses(index)"
data-testid="stage-background"
>
<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"
v-for="(stage, index) in pipelineStages"
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
<job-pill
v-for="group in stage.groups"
:key="group.name"
:job-name="group.name"
:is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
:is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
@on-mouse-enter="highlightNeeds"
@on-mouse-leave="removeHighlightNeeds"
/>
<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="getStageBackgroundClasses(index)"
data-testid="stage-background"
>
<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"
:pipeline-id="$options.PIPELINE_ID"
:is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
:is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
@on-mouse-enter="setHoveredJob"
@on-mouse-leave="removeHoveredJob"
/>
</div>
</div>
</div>
</links-layer>
</div>
</div>
</template>
......@@ -98,6 +98,42 @@ export const pipelineData = {
],
};
export const invalidNeedsData = {
stages: [
{
name: 'build',
groups: [
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
},
],
},
{
name: 'test',
groups: [
{
name: 'test_1',
jobs: [{ script: 'yarn test', stage: 'test' }],
},
{
name: 'test_2',
jobs: [{ script: 'yarn karma', stage: 'test' }],
},
],
},
{
name: 'deploy',
groups: [
{
name: 'deploy_1',
jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['invalid_job'] }],
},
],
},
],
};
export const parallelNeedData = {
stages: [
{
......
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
import { pipelineData, singleStageData } from './mock_data';
import { invalidNeedsData, pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
......@@ -16,19 +18,28 @@ describe('pipeline graph component', () => {
propsData: {
...props,
},
stubs: { LinksLayer, LinksInner },
data() {
return {
measurements: {
width: 1000,
height: 1000,
},
};
},
});
};
const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
const findAlert = () => wrapper.find(GlAlert);
const findAllStagePills = () => wrapper.findAll(StagePill);
const findAlert = () => wrapper.findComponent(GlAlert);
const findAllJobPills = () => wrapper.findAll(JobPill);
const findAllStageBackgroundElements = () => wrapper.findAll('[data-testid="stage-background"]');
const findAllStagePills = () => wrapper.findAllComponents(StagePill);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
const findStageBackgroundElementAt = (index) => findAllStageBackgroundElements().at(index);
const findAllJobPills = () => wrapper.findAll(JobPill);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with no data', () => {
......@@ -36,7 +47,7 @@ describe('pipeline graph component', () => {
wrapper = createComponent({ pipelineData: {} });
});
it('renders an empty section', () => {
it('does not render the graph', () => {
expect(wrapper.text()).toBe(wrapper.vm.$options.errorTexts[EMPTY_PIPELINE_DATA]);
expect(findPipelineGraph().exists()).toBe(false);
expect(findAllStagePills()).toHaveLength(0);
......@@ -74,10 +85,11 @@ describe('pipeline graph component', () => {
describe('with error while rendering the links with needs', () => {
beforeEach(() => {
wrapper = createComponent();
wrapper = createComponent({ pipelineData: invalidNeedsData });
});
it('renders the error that link could not be drawn', () => {
expect(findLinksLayer().exists()).toBe(true);
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[DRAW_FAILURE]);
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment