Commit 1e35547a authored by Payton Burdette's avatar Payton Burdette Committed by Andrew Fontaine

Downstream pipeline ux improvements

New tooltip text data, update the
child pipeline title, remove tooltips
on downstream pipeline labels, add multi
project label. Add hover correlation between
trigger job and downstream pipeline.
parent 0d9362bf
...@@ -43,6 +43,7 @@ export default { ...@@ -43,6 +43,7 @@ export default {
data() { data() {
return { return {
downstreamMarginTop: null, downstreamMarginTop: null,
jobName: null,
}; };
}, },
computed: { computed: {
...@@ -91,13 +92,9 @@ export default { ...@@ -91,13 +92,9 @@ export default {
/** /**
* Calculates the margin top of the clicked downstream pipeline by * Calculates the margin top of the clicked downstream pipeline by
* subtracting the clicked downstream pipelines offsetTop by it's parent's * subtracting the clicked downstream pipelines offsetTop by it's parent's
* offsetTop and then subtracting either 15 (if child) or 30 (if not a child) * offsetTop and then subtracting 15
* due to the height of node and stage name margin bottom.
*/ */
this.downstreamMarginTop = this.calculateMarginTop( this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
downstreamNode,
downstreamNode.classList.contains('child-pipeline') ? 15 : 30,
);
/** /**
* If the expanded trigger is defined and the id is different than the * If the expanded trigger is defined and the id is different than the
...@@ -120,6 +117,9 @@ export default { ...@@ -120,6 +117,9 @@ export default {
hasUpstream(index) { hasUpstream(index) {
return index === 0 && this.hasTriggeredBy; return index === 0 && this.hasTriggeredBy;
}, },
setJob(jobName) {
this.jobName = jobName;
},
}, },
}; };
</script> </script>
...@@ -180,6 +180,7 @@ export default { ...@@ -180,6 +180,7 @@ export default {
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy" :has-triggered-by="hasTriggeredBy"
:action="stage.status.action" :action="stage.status.action"
:job-hovered="jobName"
@refreshPipelineGraph="refreshPipelineGraph" @refreshPipelineGraph="refreshPipelineGraph"
/> />
</ul> </ul>
...@@ -191,6 +192,7 @@ export default { ...@@ -191,6 +192,7 @@ export default {
:project-id="pipelineProjectId" :project-id="pipelineProjectId"
graph-position="right" graph-position="right"
@linkedPipelineClick="handleClickedDownstream" @linkedPipelineClick="handleClickedDownstream"
@downstreamHovered="setJob"
/> />
<pipeline-graph <pipeline-graph
......
...@@ -31,6 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; ...@@ -31,6 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
*/ */
export default { export default {
hoverClass: 'gl-inset-border-1-blue-500',
components: { components: {
ActionComponent, ActionComponent,
JobNameComponent, JobNameComponent,
...@@ -55,6 +56,11 @@ export default { ...@@ -55,6 +56,11 @@ export default {
required: false, required: false,
default: Infinity, default: Infinity,
}, },
jobHovered: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
boundary() { boundary() {
...@@ -95,6 +101,11 @@ export default { ...@@ -95,6 +101,11 @@ export default {
hasAction() { hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path; return this.job.status && this.job.status.action && this.job.status.action.path;
}, },
jobClasses() {
return this.job.name === this.jobHovered
? `${this.$options.hoverClass} ${this.cssClassJobName}`
: this.cssClassJobName;
},
}, },
methods: { methods: {
pipelineActionRequestComplete() { pipelineActionRequestComplete() {
...@@ -120,8 +131,9 @@ export default { ...@@ -120,8 +131,9 @@ export default {
v-else v-else
v-gl-tooltip="{ boundary, placement: 'bottom' }" v-gl-tooltip="{ boundary, placement: 'bottom' }"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="jobClasses"
class="js-job-component-tooltip non-details-job-component" class="js-job-component-tooltip non-details-job-component"
data-testid="job-without-link"
> >
<job-name-component :name="job.name" :status="job.status" /> <job-name-component :name="job.name" :status="job.status" />
</div> </div>
......
<script> <script>
import { GlLoadingIcon, GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui'; import { GlLoadingIcon, GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue'; import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
directives: { directives: {
...@@ -28,7 +28,8 @@ export default { ...@@ -28,7 +28,8 @@ export default {
}, },
computed: { computed: {
tooltipText() { tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`; return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label}
${this.sourceJobInfo}`;
}, },
buttonId() { buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`; return `js-linked-pipeline-${this.pipeline.id}`;
...@@ -39,25 +40,32 @@ export default { ...@@ -39,25 +40,32 @@ export default {
projectName() { projectName() {
return this.pipeline.project.name; return this.pipeline.project.name;
}, },
downstreamTitle() {
return this.childPipeline ? __('child-pipeline') : this.pipeline.project.name;
},
parentPipeline() { parentPipeline() {
// Refactor string match when BE returns Upstream/Downstream indicators // Refactor string match when BE returns Upstream/Downstream indicators
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Upstream'); return this.projectId === this.pipeline.project.id && this.columnTitle === __('Upstream');
}, },
childPipeline() { childPipeline() {
// Refactor string match when BE returns Upstream/Downstream indicators // Refactor string match when BE returns Upstream/Downstream indicators
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Downstream'); return this.projectId === this.pipeline.project.id && this.isDownstream;
}, },
label() { label() {
return this.parentPipeline ? __('Parent') : __('Child'); if (this.parentPipeline) {
}, return __('Parent');
childTooltipText() { } else if (this.childPipeline) {
return __('This pipeline was triggered by a parent pipeline'); return __('Child');
}
return __('Multi-project');
}, },
parentTooltipText() { isDownstream() {
return __('This pipeline triggered a child pipeline'); return this.columnTitle === __('Downstream');
}, },
labelToolTipText() { sourceJobInfo() {
return this.label === __('Parent') ? this.parentTooltipText : this.childTooltipText; return this.isDownstream
? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name })
: '';
}, },
}, },
methods: { methods: {
...@@ -68,6 +76,12 @@ export default { ...@@ -68,6 +76,12 @@ export default {
hideTooltips() { hideTooltips() {
this.$root.$emit('bv::hide::tooltip'); this.$root.$emit('bv::hide::tooltip');
}, },
onDownstreamHovered() {
this.$emit('downstreamHovered', this.pipeline.source_job.name);
},
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
},
}, },
}; };
</script> </script>
...@@ -76,8 +90,10 @@ export default { ...@@ -76,8 +90,10 @@ export default {
<li <li
ref="linkedPipeline" ref="linkedPipeline"
class="linked-pipeline build" class="linked-pipeline build"
:class="{ 'child-pipeline': childPipeline }" :class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline" data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
> >
<gl-deprecated-button <gl-deprecated-button
:id="buttonId" :id="buttonId"
...@@ -95,15 +111,9 @@ export default { ...@@ -95,15 +111,9 @@ export default {
css-classes="position-top-0" css-classes="position-top-0"
class="js-linked-pipeline-status" class="js-linked-pipeline-status"
/> />
<span class="str-truncated align-bottom"> {{ projectName }} &#8226; #{{ pipeline.id }} </span> <span class="str-truncated"> {{ downstreamTitle }} &#8226; #{{ pipeline.id }} </span>
<div v-if="parentPipeline || childPipeline" class="parent-child-label-container"> <div class="gl-pt-2">
<span <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
v-gl-tooltip.bottom
:title="labelToolTipText"
class="badge badge-primary"
@mouseover="hideTooltips"
>{{ label }}</span
>
</div> </div>
</gl-deprecated-button> </gl-deprecated-button>
</li> </li>
......
...@@ -41,6 +41,9 @@ export default { ...@@ -41,6 +41,9 @@ export default {
onPipelineClick(downstreamNode, pipeline, index) { onPipelineClick(downstreamNode, pipeline, index) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode); this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
}, },
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
},
}, },
}; };
</script> </script>
...@@ -61,6 +64,7 @@ export default { ...@@ -61,6 +64,7 @@ export default {
:column-title="columnTitle" :column-title="columnTitle"
:project-id="projectId" :project-id="projectId"
@pipelineClicked="onPipelineClick($event, pipeline, index)" @pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
/> />
</ul> </ul>
</div> </div>
......
...@@ -36,6 +36,11 @@ export default { ...@@ -36,6 +36,11 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
jobHovered: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
hasAction() { hasAction() {
...@@ -80,6 +85,7 @@ export default { ...@@ -80,6 +85,7 @@ export default {
<job-item <job-item
v-if="group.size === 1" v-if="group.size === 1"
:job="group.jobs[0]" :job="group.jobs[0]"
:job-hovered="jobHovered"
css-class-job-name="build-content" css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
......
...@@ -1101,7 +1101,3 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -1101,7 +1101,3 @@ button.mini-pipeline-graph-dropdown-toggle {
.progress-bar.bg-primary { .progress-bar.bg-primary {
background-color: $blue-500 !important; background-color: $blue-500 !important;
} }
.parent-child-label-container {
padding-top: $gl-padding-4;
}
---
title: Add correlation between trigger job and child pipeline
merge_request: 36750
author:
type: changed
...@@ -130,7 +130,7 @@ ...@@ -130,7 +130,7 @@
} }
.linked-pipeline.build { .linked-pipeline.build {
height: 41px; height: 42px;
&::before { &::before {
content: none; content: none;
...@@ -163,7 +163,7 @@ ...@@ -163,7 +163,7 @@
} }
} }
&.child-pipeline { &.downstream-pipeline {
height: 68px; height: 68px;
} }
......
...@@ -6998,6 +6998,9 @@ msgstr "" ...@@ -6998,6 +6998,9 @@ msgstr ""
msgid "Created branch '%{branch_name}' and a merge request to resolve this issue." msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
msgstr "" msgstr ""
msgid "Created by %{job}"
msgstr ""
msgid "Created by me" msgid "Created by me"
msgstr "" msgstr ""
...@@ -15318,6 +15321,9 @@ msgstr "" ...@@ -15318,6 +15321,9 @@ msgstr ""
msgid "MrDeploymentActions|Stop environment" msgid "MrDeploymentActions|Stop environment"
msgstr "" msgstr ""
msgid "Multi-project"
msgstr ""
msgid "Multiple IP address ranges are supported." msgid "Multiple IP address ranges are supported."
msgstr "" msgstr ""
...@@ -24245,12 +24251,6 @@ msgstr "" ...@@ -24245,12 +24251,6 @@ msgstr ""
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>" msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>"
msgstr "" msgstr ""
msgid "This pipeline triggered a child pipeline"
msgstr ""
msgid "This pipeline was triggered by a parent pipeline"
msgstr ""
msgid "This pipeline was triggered by a schedule." msgid "This pipeline was triggered by a schedule."
msgstr "" msgstr ""
...@@ -27605,6 +27605,9 @@ msgstr "" ...@@ -27605,6 +27605,9 @@ msgstr ""
msgid "cannot merge" msgid "cannot merge"
msgstr "" msgstr ""
msgid "child-pipeline"
msgstr ""
msgid "ciReport|%{degradedNum} degraded" msgid "ciReport|%{degradedNum} degraded"
msgstr "" msgstr ""
......
...@@ -5,6 +5,8 @@ import JobItem from '~/pipelines/components/graph/job_item.vue'; ...@@ -5,6 +5,8 @@ import JobItem from '~/pipelines/components/graph/job_item.vue';
describe('pipeline graph job item', () => { describe('pipeline graph job item', () => {
let wrapper; let wrapper;
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
const createWrapper = propsData => { const createWrapper = propsData => {
wrapper = mount(JobItem, { wrapper = mount(JobItem, {
propsData, propsData,
...@@ -57,7 +59,7 @@ describe('pipeline graph job item', () => { ...@@ -57,7 +59,7 @@ describe('pipeline graph job item', () => {
}); });
describe('name without link', () => { describe('name without link', () => {
it('it should render status and name', () => { beforeEach(() => {
createWrapper({ createWrapper({
job: { job: {
id: 4257, id: 4257,
...@@ -71,13 +73,22 @@ describe('pipeline graph job item', () => { ...@@ -71,13 +73,22 @@ describe('pipeline graph job item', () => {
has_details: false, has_details: false,
}, },
}, },
cssClassJobName: 'css-class-job-name',
jobHovered: 'test',
});
}); });
it('it should render status and name', () => {
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
expect(wrapper.find('a').exists()).toBe(false); expect(wrapper.find('a').exists()).toBe(false);
expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name); expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name);
}); });
it('should apply hover class and provided class name', () => {
expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500');
expect(findJobWithoutLink().classes()).toContain('css-class-job-name');
});
}); });
describe('action icon', () => { describe('action icon', () => {
......
...@@ -11,7 +11,10 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5; ...@@ -11,7 +11,10 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
describe('Linked pipeline', () => { describe('Linked pipeline', () => {
let wrapper; let wrapper;
const findButton = () => wrapper.find('button'); const findButton = () => wrapper.find('button');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const createWrapper = propsData => { const createWrapper = propsData => {
wrapper = mount(LinkedPipelineComponent, { wrapper = mount(LinkedPipelineComponent, {
...@@ -69,6 +72,8 @@ describe('Linked pipeline', () => { ...@@ -69,6 +72,8 @@ describe('Linked pipeline', () => {
it('should correctly compute the tooltip text', () => { it('should correctly compute the tooltip text', () => {
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name); expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label); expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
}); });
it('should render the tooltip text as the title attribute', () => { it('should render the tooltip text as the title attribute', () => {
...@@ -83,9 +88,8 @@ describe('Linked pipeline', () => { ...@@ -83,9 +88,8 @@ describe('Linked pipeline', () => {
expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false); expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false);
}); });
it('should not display child label when pipeline project id is not the same as triggered pipeline project id', () => { it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
const labelContainer = wrapper.find('.parent-child-label-container'); expect(findPipelineLabel().text()).toBe('Multi-project');
expect(labelContainer.exists()).toBe(false);
}); });
}); });
...@@ -103,17 +107,17 @@ describe('Linked pipeline', () => { ...@@ -103,17 +107,17 @@ describe('Linked pipeline', () => {
it('parent/child label container should exist', () => { it('parent/child label container should exist', () => {
createWrapper(downstreamProps); createWrapper(downstreamProps);
expect(wrapper.find('.parent-child-label-container').exists()).toBe(true); expect(findPipelineLabel().exists()).toBe(true);
}); });
it('should display child label when pipeline project id is the same as triggered pipeline project id', () => { it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
createWrapper(downstreamProps); createWrapper(downstreamProps);
expect(wrapper.find('.parent-child-label-container').text()).toContain('Child'); expect(findPipelineLabel().exists()).toBe(true);
}); });
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
createWrapper(upstreamProps); createWrapper(upstreamProps);
expect(wrapper.find('.parent-child-label-container').text()).toContain('Parent'); expect(findPipelineLabel().exists()).toBe(true);
}); });
}); });
...@@ -133,7 +137,7 @@ describe('Linked pipeline', () => { ...@@ -133,7 +137,7 @@ describe('Linked pipeline', () => {
}); });
}); });
describe('on click', () => { describe('on click/hover', () => {
const props = { const props = {
pipeline: mockPipeline, pipeline: mockPipeline,
projectId: validTriggeredPipelineId, projectId: validTriggeredPipelineId,
...@@ -160,5 +164,15 @@ describe('Linked pipeline', () => { ...@@ -160,5 +164,15 @@ describe('Linked pipeline', () => {
'js-linked-pipeline-34993051', 'js-linked-pipeline-34993051',
]); ]);
}); });
it('should emit downstreamHovered with job name on mouseover', () => {
findLinkedPipeline().trigger('mouseover');
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]);
});
it('should emit downstreamHovered with empty string on mouseleave', () => {
findLinkedPipeline().trigger('mouseleave');
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]);
});
}); });
}); });
...@@ -14,6 +14,9 @@ export default { ...@@ -14,6 +14,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'push', source: 'push',
source_job: {
name: 'trigger_job',
},
created_at: '2018-06-05T11:31:30.452Z', created_at: '2018-06-05T11:31:30.452Z',
updated_at: '2018-10-31T16:35:31.305Z', updated_at: '2018-10-31T16:35:31.305Z',
path: '/gitlab-org/gitlab-runner/pipelines/23211253', path: '/gitlab-org/gitlab-runner/pipelines/23211253',
...@@ -381,6 +384,9 @@ export default { ...@@ -381,6 +384,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'pipeline', source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051', path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: { details: {
status: { status: {
...@@ -889,6 +895,9 @@ export default { ...@@ -889,6 +895,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'pipeline', source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051', path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: { details: {
status: { status: {
...@@ -1402,6 +1411,9 @@ export default { ...@@ -1402,6 +1411,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'pipeline', source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051', path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: { details: {
status: { status: {
...@@ -1912,6 +1924,9 @@ export default { ...@@ -1912,6 +1924,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'pipeline', source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051', path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: { details: {
status: { status: {
...@@ -2412,6 +2427,9 @@ export default { ...@@ -2412,6 +2427,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'push', source: 'push',
source_job: {
name: 'trigger_job',
},
created_at: '2019-01-06T17:48:37.599Z', created_at: '2019-01-06T17:48:37.599Z',
updated_at: '2019-01-06T17:48:38.371Z', updated_at: '2019-01-06T17:48:38.371Z',
path: '/h5bp/html5-boilerplate/pipelines/26', path: '/h5bp/html5-boilerplate/pipelines/26',
...@@ -3743,6 +3761,9 @@ export default { ...@@ -3743,6 +3761,9 @@ export default {
active: false, active: false,
coverage: null, coverage: null,
source: 'push', source: 'push',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-org/gitlab-test/pipelines/4', path: '/gitlab-org/gitlab-test/pipelines/4',
details: { details: {
status: { status: {
......
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