Commit 152d1286 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'parent-child-upstream-downstream-labels' into 'master'

Add child and parent labels to pipelines (3/3)

See merge request gitlab-org/gitlab!21332
parents 035c7ad7 e7b2d546
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
downstream: 'downstream', downstream: 'downstream',
data() { data() {
return { return {
triggeredTopIndex: 1, downstreamMarginTop: null,
}; };
}, },
computed: { computed: {
...@@ -77,26 +77,34 @@ export default { ...@@ -77,26 +77,34 @@ export default {
expandedTriggered() { expandedTriggered() {
return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded); return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded);
}, },
/**
* Calculates the margin top of the clicked downstream pipeline by
* adding the height of each linked pipeline and the margin
*/
marginTop() {
return `${this.triggeredTopIndex * 52}px`;
},
pipelineTypeUpstream() { pipelineTypeUpstream() {
return this.type !== this.$options.downstream && this.expandedTriggeredBy; return this.type !== this.$options.downstream && this.expandedTriggeredBy;
}, },
pipelineTypeDownstream() { pipelineTypeDownstream() {
return this.type !== this.$options.upstream && this.expandedTriggered; return this.type !== this.$options.upstream && this.expandedTriggered;
}, },
pipelineProjectId() {
return this.pipeline.project.id;
},
}, },
methods: { methods: {
handleClickedDownstream(pipeline, clickedIndex) { handleClickedDownstream(pipeline, clickedIndex, downstreamNode) {
this.triggeredTopIndex = clickedIndex; /**
* 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.
*/
this.downstreamMarginTop = this.calculateMarginTop(
downstreamNode,
downstreamNode.classList.contains('child-pipeline') ? 15 : 30,
);
this.$emit('onClickTriggered', this.pipeline, pipeline); this.$emit('onClickTriggered', this.pipeline, pipeline);
}, },
calculateMarginTop(downstreamNode, pixelDiff) {
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
},
hasOnlyOneJob(stage) { hasOnlyOneJob(stage) {
return stage.groups.length === 1; return stage.groups.length === 1;
}, },
...@@ -139,6 +147,7 @@ export default { ...@@ -139,6 +147,7 @@ export default {
v-if="hasTriggeredBy" v-if="hasTriggeredBy"
:linked-pipelines="triggeredByPipelines" :linked-pipelines="triggeredByPipelines"
:column-title="__('Upstream')" :column-title="__('Upstream')"
:project-id="pipelineProjectId"
graph-position="left" graph-position="left"
@linkedPipelineClick=" @linkedPipelineClick="
linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline) linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
...@@ -174,6 +183,7 @@ export default { ...@@ -174,6 +183,7 @@ export default {
v-if="hasTriggered" v-if="hasTriggered"
:linked-pipelines="triggeredPipelines" :linked-pipelines="triggeredPipelines"
:column-title="__('Downstream')" :column-title="__('Downstream')"
:project-id="pipelineProjectId"
graph-position="right" graph-position="right"
@linkedPipelineClick="handleClickedDownstream" @linkedPipelineClick="handleClickedDownstream"
/> />
...@@ -186,7 +196,7 @@ export default { ...@@ -186,7 +196,7 @@ export default {
:is-loading="false" :is-loading="false"
:pipeline="expandedTriggered" :pipeline="expandedTriggered"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:style="{ 'margin-top': marginTop }" :style="{ 'margin-top': downstreamMarginTop }"
:mediator="mediator" :mediator="mediator"
@onClickTriggered=" @onClickTriggered="
(parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline) (parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
......
<script> <script>
import { GlLoadingIcon, GlTooltipDirective, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue'; import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __ } from '~/locale';
export default { export default {
directives: { directives: {
...@@ -16,6 +17,14 @@ export default { ...@@ -16,6 +17,14 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
},
columnTitle: {
type: String,
required: true,
},
}, },
computed: { computed: {
tooltipText() { tooltipText() {
...@@ -30,18 +39,45 @@ export default { ...@@ -30,18 +39,45 @@ export default {
projectName() { projectName() {
return this.pipeline.project.name; return 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');
},
label() {
return this.parentPipeline ? __('Parent') : __('Child');
},
childTooltipText() {
return __('This pipeline was triggered by a parent pipeline');
},
parentTooltipText() {
return __('This pipeline triggered a child pipeline');
},
labelToolTipText() {
return this.label === __('Parent') ? this.parentTooltipText : this.childTooltipText;
},
}, },
methods: { methods: {
onClickLinkedPipeline() { onClickLinkedPipeline() {
this.$root.$emit('bv::hide::tooltip', this.buttonId); this.$root.$emit('bv::hide::tooltip', this.buttonId);
this.$emit('pipelineClicked'); this.$emit('pipelineClicked', this.$refs.linkedPipeline);
},
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
}, },
}, },
}; };
</script> </script>
<template> <template>
<li class="linked-pipeline build"> <li
ref="linkedPipeline"
class="linked-pipeline build"
:class="{ 'child-pipeline': childPipeline }"
>
<gl-button <gl-button
:id="buttonId" :id="buttonId"
v-gl-tooltip v-gl-tooltip
...@@ -59,6 +95,15 @@ export default { ...@@ -59,6 +95,15 @@ export default {
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 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
>
</div>
</gl-button> </gl-button>
</li> </li>
</template> </template>
...@@ -19,6 +19,10 @@ export default { ...@@ -19,6 +19,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
},
}, },
computed: { computed: {
columnClass() { columnClass() {
...@@ -28,10 +32,16 @@ export default { ...@@ -28,10 +32,16 @@ export default {
}; };
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
}, },
// Refactor string match when BE returns Upstream/Downstream indicators
isUpstream() { isUpstream() {
return this.columnTitle === __('Upstream'); return this.columnTitle === __('Upstream');
}, },
}, },
methods: {
onPipelineClick(downstreamNode, pipeline, index) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
},
},
}; };
</script> </script>
...@@ -48,7 +58,9 @@ export default { ...@@ -48,7 +58,9 @@ export default {
'left-connector': pipeline.isExpanded && graphPosition === 'left', 'left-connector': pipeline.isExpanded && graphPosition === 'left',
}" }"
:pipeline="pipeline" :pipeline="pipeline"
@pipelineClicked="$emit('linkedPipelineClick', pipeline, index)" :column-title="columnTitle"
:project-id="projectId"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
/> />
</ul> </ul>
</div> </div>
......
...@@ -1093,3 +1093,7 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -1093,3 +1093,7 @@ 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 child and parent labels to pipelines
merge_request: 21332
author:
type: added
...@@ -163,6 +163,10 @@ ...@@ -163,6 +163,10 @@
} }
} }
&.child-pipeline {
height: 68px;
}
.linked-pipeline-content { .linked-pipeline-content {
@include build-content(0); @include build-content(0);
text-align: inherit; text-align: inherit;
......
...@@ -3344,6 +3344,9 @@ msgstr "" ...@@ -3344,6 +3344,9 @@ msgstr ""
msgid "Cherry-pick this merge request" msgid "Cherry-pick this merge request"
msgstr "" msgstr ""
msgid "Child"
msgstr ""
msgid "Child epic does not exist." msgid "Child epic does not exist."
msgstr "" msgstr ""
...@@ -12879,6 +12882,9 @@ msgstr "" ...@@ -12879,6 +12882,9 @@ msgstr ""
msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}" msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
msgstr "" msgstr ""
msgid "Parent"
msgstr ""
msgid "Parent epic doesn't exist." msgid "Parent epic doesn't exist."
msgstr "" msgstr ""
...@@ -18878,6 +18884,12 @@ msgstr "" ...@@ -18878,6 +18884,12 @@ 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 project" msgid "This project"
msgstr "" msgstr ""
......
...@@ -8,6 +8,13 @@ const mockPipeline = mockData.triggered[0]; ...@@ -8,6 +8,13 @@ const mockPipeline = mockData.triggered[0];
describe('Linked pipeline', () => { describe('Linked pipeline', () => {
let wrapper; let wrapper;
const createWrapper = propsData => {
wrapper = mount(LinkedPipelineComponent, {
attachToDocument: true,
propsData,
});
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -15,13 +22,12 @@ describe('Linked pipeline', () => { ...@@ -15,13 +22,12 @@ describe('Linked pipeline', () => {
describe('rendered output', () => { describe('rendered output', () => {
const props = { const props = {
pipeline: mockPipeline, pipeline: mockPipeline,
projectId: 20,
columnTitle: 'Downstream',
}; };
beforeEach(() => { beforeEach(() => {
wrapper = mount(LinkedPipelineComponent, { createWrapper(props);
attachToDocument: true,
propsData: props,
});
}); });
it('should render a list item as the containing element', () => { it('should render a list item as the containing element', () => {
...@@ -73,18 +79,50 @@ describe('Linked pipeline', () => { ...@@ -73,18 +79,50 @@ describe('Linked pipeline', () => {
it('does not render the loading icon when isLoading is false', () => { it('does not render the loading icon when isLoading is false', () => {
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', () => {
const labelContainer = wrapper.find('.parent-child-label-container');
expect(labelContainer.exists()).toBe(false);
});
});
describe('parent/child', () => {
const downstreamProps = {
pipeline: mockPipeline,
projectId: 19,
columnTitle: 'Downstream',
};
const upstreamProps = {
...downstreamProps,
columnTitle: 'Upstream',
};
it('parent/child label container should exist', () => {
createWrapper(downstreamProps);
expect(wrapper.find('.parent-child-label-container').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');
});
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');
});
}); });
describe('when isLoading is true', () => { describe('when isLoading is true', () => {
const props = { const props = {
pipeline: { ...mockPipeline, isLoading: true }, pipeline: { ...mockPipeline, isLoading: true },
projectId: 19,
columnTitle: 'Downstream',
}; };
beforeEach(() => { beforeEach(() => {
wrapper = mount(LinkedPipelineComponent, { createWrapper(props);
attachToDocument: true,
propsData: props,
});
}); });
it('renders a loading icon', () => { it('renders a loading icon', () => {
...@@ -95,20 +133,19 @@ describe('Linked pipeline', () => { ...@@ -95,20 +133,19 @@ describe('Linked pipeline', () => {
describe('on click', () => { describe('on click', () => {
const props = { const props = {
pipeline: mockPipeline, pipeline: mockPipeline,
projectId: 19,
columnTitle: 'Downstream',
}; };
beforeEach(() => { beforeEach(() => {
wrapper = mount(LinkedPipelineComponent, { createWrapper(props);
attachToDocument: true,
propsData: props,
});
}); });
it('emits `pipelineClicked` event', () => { it('emits `pipelineClicked` event', () => {
jest.spyOn(wrapper.vm, '$emit'); jest.spyOn(wrapper.vm, '$emit');
wrapper.find('button').trigger('click'); wrapper.find('button').trigger('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineClicked'); expect(wrapper.emitted().pipelineClicked).toBeTruthy();
}); });
it('should emit `bv::hide::tooltip` to close the tooltip', () => { it('should emit `bv::hide::tooltip` to close the tooltip', () => {
......
export default { export default {
project: {
id: 19,
},
triggered_by: { triggered_by: {
id: 129, id: 129,
active: true, active: true,
...@@ -63,6 +66,7 @@ export default { ...@@ -63,6 +66,7 @@ export default {
path: '/gitlab-org/gitlab-foss/pipelines/132', path: '/gitlab-org/gitlab-foss/pipelines/132',
project: { project: {
name: 'GitLabCE', name: 'GitLabCE',
id: 19,
}, },
details: { details: {
status: { status: {
......
...@@ -190,6 +190,7 @@ describe('graph component', () => { ...@@ -190,6 +190,7 @@ describe('graph component', () => {
describe('on click', () => { describe('on click', () => {
it('should emit `onClickTriggered`', () => { it('should emit `onClickTriggered`', () => {
spyOn(component, '$emit'); spyOn(component, '$emit');
spyOn(component, 'calculateMarginTop').and.callFake(() => '16px');
component.$el.querySelector('#js-linked-pipeline-34993051').click(); component.$el.querySelector('#js-linked-pipeline-34993051').click();
......
...@@ -9,6 +9,7 @@ describe('Linked Pipelines Column', () => { ...@@ -9,6 +9,7 @@ describe('Linked Pipelines Column', () => {
columnTitle: 'Upstream', columnTitle: 'Upstream',
linkedPipelines: mockData.triggered, linkedPipelines: mockData.triggered,
graphPosition: 'right', graphPosition: 'right',
projectId: 19,
}; };
let vm; let vm;
......
...@@ -341,6 +341,9 @@ ...@@ -341,6 +341,9 @@
"commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46", "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
"commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46" "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
}, },
"project": {
"id": 1794617
},
"triggered_by": { "triggered_by": {
"id": 12, "id": 12,
"user": { "user": {
......
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