Commit a16a1ec9 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo Committed by Andrew Fontaine

Add link capability to graph

Includes new component, specs
parent edfe34ba
...@@ -39,6 +39,7 @@ export default { ...@@ -39,6 +39,7 @@ export default {
data() { data() {
return { return {
hoveredJobName: '', hoveredJobName: '',
highlightedJobs: [],
measurements: { measurements: {
width: 0, width: 0,
height: 0, height: 0,
...@@ -106,63 +107,68 @@ export default { ...@@ -106,63 +107,68 @@ export default {
jobName: expanded ? jobName : '', jobName: expanded ? jobName : '',
}; };
}, },
updateHighlightedJobs(jobs) {
this.highlightedJobs = jobs;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="js-pipeline-graph"> <div class="js-pipeline-graph">
<div <div
:id="containerId" class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
:ref="containerId" :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }"
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 }"
> >
<links-layer <linked-graph-wrapper>
:pipeline-data="graph" <template #upstream>
:pipeline-id="pipeline.id" <linked-pipelines-column
:container-id="containerId" v-if="showUpstreamPipelines"
:container-measurements="measurements" :linked-pipelines="upstreamPipelines"
:highlighted-job="hoveredJobName" :column-title="__('Upstream')"
default-link-color="gl-stroke-transparent" :type="$options.pipelineTypeConstants.UPSTREAM"
@error="onError" @error="onError"
> />
<linked-graph-wrapper> </template>
<template #upstream> <template #main>
<linked-pipelines-column <div :id="containerId" :ref="containerId">
v-if="showUpstreamPipelines" <links-layer
:linked-pipelines="upstreamPipelines" :pipeline-data="graph"
: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" :pipeline-id="pipeline.id"
@refreshPipelineGraph="$emit('refreshPipelineGraph')" :container-id="containerId"
@jobHover="setJob" :container-measurements="measurements"
/> :highlighted-job="hoveredJobName"
</template> default-link-color="gl-stroke-transparent"
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
@downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded"
@error="onError" @error="onError"
/> @highlightedJobsChange="updateHighlightedJobs"
</template> >
</linked-graph-wrapper> <stage-column-component
</links-layer> v-for="stage in graph"
:key="stage.name"
:title="stage.name"
:groups="stage.groups"
:action="stage.status.action"
:highlighted-jobs="highlightedJobs"
:job-hovered="hoveredJobName"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipeline.id"
@refreshPipelineGraph="$emit('refreshPipelineGraph')"
@jobHover="setJob"
/>
</links-layer>
</div>
</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>
</div> </div>
</div> </div>
</template> </template>
...@@ -42,8 +42,8 @@ export default { ...@@ -42,8 +42,8 @@ export default {
computed: { computed: {
columnClass() { columnClass() {
const positionValues = { const positionValues = {
right: 'gl-ml-11', right: 'gl-ml-6',
left: 'gl-mr-7', left: 'gl-mr-6',
}; };
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
}, },
......
...@@ -16,10 +16,6 @@ export default { ...@@ -16,10 +16,6 @@ export default {
MainGraphWrapper, MainGraphWrapper,
}, },
props: { props: {
title: {
type: String,
required: true,
},
groups: { groups: {
type: Array, type: Array,
required: true, required: true,
...@@ -28,11 +24,20 @@ export default { ...@@ -28,11 +24,20 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
title: {
type: String,
required: true,
},
action: { action: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
highlightedJobs: {
type: Array,
required: false,
default: () => [],
},
jobHovered: { jobHovered: {
type: String, type: String,
required: false, required: false,
...@@ -69,11 +74,18 @@ export default { ...@@ -69,11 +74,18 @@ export default {
groupId(group) { groupId(group) {
return `ci-badge-${escape(group.name)}`; return `ci-badge-${escape(group.name)}`;
}, },
isFadedOut(jobName) {
return (
this.jobHovered &&
this.highlightedJobs.length > 1 &&
!this.highlightedJobs.includes(jobName)
);
},
}, },
}; };
</script> </script>
<template> <template>
<main-graph-wrapper> <main-graph-wrapper class="gl-px-6">
<template #stages> <template #stages>
<div <div
data-testid="stage-column-title" data-testid="stage-column-title"
...@@ -108,9 +120,15 @@ export default { ...@@ -108,9 +120,15 @@ export default {
:pipeline-expanded="pipelineExpanded" :pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId" :pipeline-id="pipelineId"
css-class-job-name="gl-build-content" css-class-job-name="gl-build-content"
:class="{ 'gl-opacity-3': isFadedOut(group.name) }"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/> />
<job-group-dropdown v-else :group="group" :pipeline-id="pipelineId" /> <job-group-dropdown
v-else
:group="group"
:pipeline-id="pipelineId"
:class="{ 'gl-opacity-3': isFadedOut(group.name) }"
/>
</div> </div>
</template> </template>
</main-graph-wrapper> </main-graph-wrapper>
......
...@@ -64,7 +64,10 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => { ...@@ -64,7 +64,10 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => {
// Make cross-stages lines a straight line all the way // Make cross-stages lines a straight line all the way
// until we can safely draw the bezier to look nice. // until we can safely draw the bezier to look nice.
const straightLineDestinationX = targetNodeX - 100; // The adjustment number here is a magic number to make things
// look nice and should change if the padding changes. This goes well
// with gl-px-6. gl-px-8 is more like 100.
const straightLineDestinationX = targetNodeX - 60;
const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2; const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2;
if (straightLineDestinationX > 0) { if (straightLineDestinationX > 0) {
......
...@@ -83,6 +83,9 @@ export default { ...@@ -83,6 +83,9 @@ export default {
this.needsObject = generateJobNeedsDict(jobs) ?? {}; this.needsObject = generateJobNeedsDict(jobs) ?? {};
} }
}, },
highlightedJobs(jobs) {
this.$emit('highlightedJobsChange', jobs);
},
}, },
mounted() { mounted() {
if (!isEmpty(this.pipelineData)) { if (!isEmpty(this.pipelineData)) {
......
...@@ -16,14 +16,11 @@ export default { ...@@ -16,14 +16,11 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<div <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses">
class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-mb-5"
:class="stageClasses"
>
<slot name="stages"> </slot> <slot name="stages"> </slot>
</div> </div>
<div <div
class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8" class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
:class="jobClasses" :class="jobClasses"
> >
<slot name="jobs"> </slot> <slot name="jobs"> </slot>
......
...@@ -148,18 +148,13 @@ ...@@ -148,18 +148,13 @@
&:hover { &:hover {
box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color; box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
background-color: $gray-darker; background-color: var(--gray-50, $gray-50);
svg {
fill: $gl-text-color;
}
} }
.spinner, .spinner,
svg { svg {
width: $ci-action-dropdown-svg-size; width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size; height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative; position: relative;
top: 1px; top: 1px;
vertical-align: initial; vertical-align: initial;
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import { GRAPHQL } from '~/pipelines/components/graph/constants'; import { GRAPHQL } from '~/pipelines/components/graph/constants';
...@@ -21,17 +22,29 @@ describe('graph component', () => { ...@@ -21,17 +22,29 @@ describe('graph component', () => {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'), pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
}; };
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { const createComponent = ({
data = {},
mountFn = shallowMount,
props = {},
stubOverride = {},
} = {}) => {
wrapper = mountFn(PipelineGraph, { wrapper = mountFn(PipelineGraph, {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
data() {
return { ...data };
},
provide: { provide: {
dataMethod: GRAPHQL, dataMethod: GRAPHQL,
}, },
stubs: { stubs: {
'links-inner': true, 'links-inner': true,
'linked-pipeline': true,
'job-item': true,
'job-group-dropdown': true,
...stubOverride,
}, },
}); });
}; };
...@@ -63,6 +76,23 @@ describe('graph component', () => { ...@@ -63,6 +76,23 @@ describe('graph component', () => {
expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1); expect(wrapper.emitted().refreshPipelineGraph).toHaveLength(1);
}); });
}); });
describe('when links are present', () => {
beforeEach(async () => {
createComponent({
mountFn: mount,
stubOverride: { 'job-item': false },
data: { hoveredJobName: 'test_a' },
});
findLinksLayer().vm.$emit('highlightedJobsChange', ['test_c', 'build_c']);
});
it('dims unrelated jobs', () => {
const unrelatedJob = wrapper.find(JobItem);
expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1);
expect(unrelatedJob.classes('gl-opacity-3')).toBe(true);
});
});
}); });
describe('when linked pipelines are not present', () => { describe('when linked pipelines are not present', () => {
......
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