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