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 { ...@@ -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;
} }
......
...@@ -7001,6 +7001,9 @@ msgstr "" ...@@ -7001,6 +7001,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 ""
...@@ -15327,6 +15330,9 @@ msgstr "" ...@@ -15327,6 +15330,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 ""
...@@ -24257,12 +24263,6 @@ msgstr "" ...@@ -24257,12 +24263,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 ""
...@@ -27617,6 +27617,9 @@ msgstr "" ...@@ -27617,6 +27617,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