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

Add hover state on jobs for ci config preview

When a user hover a job in the ci config graph,
they will see their stage and jobs highlighted.
parent 6abe2c3b
......@@ -14,14 +14,11 @@ import { createUniqueJobId } from '../../utils';
export const generateLinksData = ({ links }, jobs, containerID) => {
const containerEl = document.getElementById(containerID);
return links.map(link => {
const path = d3.path();
// We can only have one unique job name per stage, so our selector
// is: ${stageName}-${jobName}
const sourceId = createUniqueJobId(jobs[link.source].stage, link.source);
const targetId = createUniqueJobId(jobs[link.target].stage, link.target);
const sourceId = jobs[link.source].id;
const targetId = jobs[link.target].id;
const sourceNodeEl = document.getElementById(sourceId);
const targetNodeEl = document.getElementById(targetId);
......@@ -80,6 +77,12 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
targetNodeY,
);
return { ...link, path: path.toString() };
return {
...link,
source: sourceId,
target: targetId,
ref: createUniqueJobId(sourceId, targetId),
path: path.toString(),
};
});
};
......@@ -14,6 +14,42 @@ export default {
type: String,
required: true,
},
isHighlighted: {
type: Boolean,
required: false,
default: false,
},
isFadedOut: {
type: Boolean,
required: false,
default: false,
},
handleMouseOver: {
type: Function,
required: false,
default: () => {},
},
handleMouseLeave: {
type: Function,
required: false,
default: () => {},
},
},
computed: {
jobPillClasses() {
return [
{ 'gl-opacity-3': this.isFadedOut },
this.isHighlighted ? 'gl-shadow-blue-200-x0-y0-b4-s2' : 'gl-inset-border-2-green-400',
];
},
},
methods: {
onMouseEnter() {
this.$emit('on-mouse-enter', this.jobId);
},
onMouseLeave() {
this.$emit('on-mouse-leave');
},
},
};
</script>
......@@ -21,7 +57,10 @@ export default {
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
:id="jobId"
class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 pipeline-job-pill "
class="pipeline-job-pill 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"
@mouseleave="onMouseLeave"
>
{{ jobName }}
</div>
......
......@@ -7,7 +7,7 @@ import StagePill from './stage_pill.vue';
import { generateLinksData } from './drawing_utils';
import { parseData } from '../parsing_utils';
import { DRAW_FAILURE, DEFAULT } from '../../constants';
import { createUniqueJobId } from '../../utils';
import { generateJobNeedsDict } from '../../utils';
export default {
components: {
......@@ -31,7 +31,9 @@ export default {
data() {
return {
failureType: null,
highlightedJob: null,
links: [],
needsObject: null,
height: 0,
width: 0,
};
......@@ -43,6 +45,9 @@ export default {
hasError() {
return this.failureType;
},
hasHighlightedJob() {
return Boolean(this.highlightedJob);
},
failure() {
const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
......@@ -51,8 +56,27 @@ export default {
viewBox() {
return [0, 0, this.width, this.height];
},
lineStyle() {
return `stroke-width:${this.$options.STROKE_WIDTH}px;`;
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 [];
},
},
mounted() {
......@@ -62,9 +86,6 @@ export default {
}
},
methods: {
createJobId(stageName, jobName) {
return createUniqueJobId(stageName, jobName);
},
drawJobLinks() {
const { stages, jobs } = this.pipelineData;
const unwrappedGroups = this.unwrapPipelineData(stages);
......@@ -76,6 +97,18 @@ export default {
this.reportFailure(DRAW_FAILURE);
}
},
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) {
this.needsObject = generateJobNeedsDict(this.pipelineData) ?? {};
}
this.highlightedJob = uniqueJobId;
},
removeHighlightNeeds() {
this.highlightedJob = null;
},
unwrapPipelineData(stages) {
return stages
.map(({ name, groups }) => {
......@@ -95,6 +128,18 @@ export default {
resetFailure() {
this.failureType = null;
},
isJobHighlighted(jobName) {
return this.highlightedJobs.includes(jobName);
},
isLinkHighlighted(linkRef) {
return this.highlightedLinks.includes(linkRef);
},
getLinkClasses(link) {
return [
this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : 'gl-stroke-gray-200',
{ 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
];
},
},
};
</script>
......@@ -113,13 +158,17 @@ export default {
class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
>
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
<path
v-for="link in links"
:key="link.path"
:d="link.path"
class="gl-stroke-gray-200 gl-fill-transparent"
:style="lineStyle"
/>
<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>
<div
v-for="(stage, index) in pipelineData.stages"
......@@ -141,8 +190,12 @@ export default {
<job-pill
v-for="group in stage.groups"
:key="group.name"
:job-id="createJobId(stage.name, group.name)"
:job-id="group.id"
:job-name="group.name"
:is-highlighted="hasHighlightedJob && isJobHighlighted(group.id)"
:is-faded-out="hasHighlightedJob && !isJobHighlighted(group.id)"
@on-mouse-enter="highlightNeeds"
@on-mouse-leave="removeHighlightNeeds"
/>
</div>
</div>
......
......@@ -5,6 +5,8 @@ export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
/**
* This function takes a json payload that comes from a yml
* file converted to json through `jsyaml` library. Because we
......@@ -21,7 +23,10 @@ export const preparePipelineGraphData = jsonData => {
// Creates an object with only the valid jobs
const jobs = jsonKeys.reduce((acc, val) => {
if (jobNames.includes(val)) {
return { ...acc, [val]: { ...jsonData[val] } };
return {
...acc,
[val]: { ...jsonData[val], id: createUniqueJobId(jsonData[val].stage, val) },
};
}
return { ...acc };
}, {});
......@@ -47,7 +52,11 @@ export const preparePipelineGraphData = jsonData => {
return {
name: stage,
groups: stageJobs.map(job => {
return { name: job, jobs: [{ ...jsonData[job] }] };
return {
name: job,
jobs: [{ ...jsonData[job] }],
id: createUniqueJobId(stage, job),
};
}),
};
});
......@@ -55,4 +64,33 @@ export const preparePipelineGraphData = jsonData => {
return { stages: pipelineData, jobs };
};
export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
export const generateJobNeedsDict = ({ jobs }) => {
const arrOfJobNames = Object.keys(jobs);
return arrOfJobNames.reduce((acc, value) => {
const recursiveNeeds = jobName => {
if (!jobs[jobName]?.needs) {
return [];
}
return jobs[jobName].needs
.map(job => {
const { id } = jobs[job];
// If we already have the needs of a job in the accumulator,
// then we use the memoized data instead of the recursive call
// to save some performance.
const newNeeds = acc[id] ?? recursiveNeeds(job);
return [id, ...newNeeds];
})
.flat(Infinity);
};
// To ensure we don't have duplicates job relationship when 2 jobs
// needed by another both depends on the same jobs, we remove any
// duplicates from the array.
const uniqueValues = Array.from(new Set(recursiveNeeds(value)));
return { ...acc, [jobs[value].id]: uniqueValues };
}, {});
};
import { createUniqueJobId } from '~/pipelines/utils';
export const yamlString = `stages:
- empty
- build
......@@ -39,18 +41,20 @@ deploy_a:
script: echo hello
`;
const jobId1 = createUniqueJobId('build', 'build_1');
const jobId2 = createUniqueJobId('test', 'test_1');
const jobId3 = createUniqueJobId('test', 'test_2');
const jobId4 = createUniqueJobId('deploy', 'deploy_1');
export const pipelineData = {
stages: [
{
name: 'build',
groups: [],
},
{
name: 'build',
groups: [
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
id: jobId1,
},
],
},
......@@ -60,10 +64,12 @@ export const pipelineData = {
{
name: 'test_1',
jobs: [{ script: 'yarn test', stage: 'test' }],
id: jobId2,
},
{
name: 'test_2',
jobs: [{ script: 'yarn karma', stage: 'test' }],
id: jobId3,
},
],
},
......@@ -73,8 +79,15 @@ export const pipelineData = {
{
name: 'deploy_1',
jobs: [{ script: 'yarn magick', stage: 'deploy' }],
id: jobId4,
},
],
},
],
jobs: {
[jobId1]: {},
[jobId2]: {},
[jobId3]: {},
[jobId4]: {},
},
};
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