Commit f724bf57 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'downstream-pipeline-ux' into 'master'

Downstream pipeline ux improvements

See merge request gitlab-org/gitlab!36750
parents 8224727b 1e35547a
......@@ -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;
}
......
......@@ -7001,6 +7001,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 ""
......@@ -15327,6 +15330,9 @@ msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multi-project"
msgstr ""
msgid "Multiple IP address ranges are supported."
msgstr ""
......@@ -24257,12 +24263,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 ""
......@@ -27617,6 +27617,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