Commit 172823da authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Simon Knox

Refactor child/parent pipeline cards to use flexbox

Instead of moving the expand button with an absolute positioning,
we rely on flex box row and reverse row to control the direction
of the elements for downstreams and upstreams expand button. This
is more stable and will serve as a baseline to introduce better
tooltips.
parent 115add63
<script> <script>
import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue'; import CiStatus from '~/vue_shared/components/ci_icon.vue';
...@@ -12,10 +12,10 @@ export default { ...@@ -12,10 +12,10 @@ export default {
}, },
components: { components: {
CiStatus, CiStatus,
GlBadge,
GlButton, GlButton,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlBadge,
}, },
props: { props: {
columnTitle: { columnTitle: {
...@@ -26,6 +26,10 @@ export default { ...@@ -26,6 +26,10 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isLoading: {
type: Boolean,
required: true,
},
pipeline: { pipeline: {
type: Object, type: Object,
required: true, required: true,
...@@ -34,33 +38,40 @@ export default { ...@@ -34,33 +38,40 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isLoading: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
tooltipText() { buttonBorderClass() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} - return this.isUpstream ? 'gl-border-r-1!' : 'gl-border-l-1!';
${this.sourceJobInfo}`;
}, },
buttonId() { buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`; return `js-linked-pipeline-${this.pipeline.id}`;
}, },
pipelineStatus() { cardSpacingClass() {
return this.pipeline.status; return this.isDownstream ? 'gl-pr-0' : '';
}, },
projectName() { expandedIcon() {
return this.pipeline.project.name; if (this.isUpstream) {
return this.expanded ? 'angle-right' : 'angle-left';
}
return this.expanded ? 'angle-left' : 'angle-right';
},
childPipeline() {
return this.isDownstream && this.isSameProject;
}, },
downstreamTitle() { downstreamTitle() {
return this.childPipeline ? this.sourceJobName : this.pipeline.project.name; return this.childPipeline ? this.sourceJobName : this.pipeline.project.name;
}, },
parentPipeline() { flexDirection() {
return this.isUpstream && this.isSameProject; return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
}, },
childPipeline() { isDownstream() {
return this.isDownstream && this.isSameProject; return this.type === DOWNSTREAM;
},
isSameProject() {
return !this.pipeline.multiproject;
},
isUpstream() {
return this.type === UPSTREAM;
}, },
label() { label() {
if (this.parentPipeline) { if (this.parentPipeline) {
...@@ -70,17 +81,17 @@ export default { ...@@ -70,17 +81,17 @@ export default {
} }
return __('Multi-project'); return __('Multi-project');
}, },
parentPipeline() {
return this.isUpstream && this.isSameProject;
},
pipelineIsLoading() { pipelineIsLoading() {
return Boolean(this.isLoading || this.pipeline.isLoading); return Boolean(this.isLoading || this.pipeline.isLoading);
}, },
isDownstream() { pipelineStatus() {
return this.type === DOWNSTREAM; return this.pipeline.status;
},
isUpstream() {
return this.type === UPSTREAM;
}, },
isSameProject() { projectName() {
return !this.pipeline.multiproject; return this.pipeline.project.name;
}, },
sourceJobName() { sourceJobName() {
return this.pipeline.sourceJob?.name ?? ''; return this.pipeline.sourceJob?.name ?? '';
...@@ -88,28 +99,23 @@ export default { ...@@ -88,28 +99,23 @@ export default {
sourceJobInfo() { sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : ''; return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
}, },
expandedIcon() { tooltipText() {
if (this.isUpstream) { return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
return this.expanded ? 'angle-right' : 'angle-left'; ${this.sourceJobInfo}`;
}
return this.expanded ? 'angle-left' : 'angle-right';
},
expandButtonPosition() {
return this.isUpstream ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!';
}, },
}, },
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`); reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
}, },
methods: { methods: {
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
onClickLinkedPipeline() { onClickLinkedPipeline() {
this.hideTooltips(); this.hideTooltips();
this.$emit('pipelineClicked', this.$refs.linkedPipeline); this.$emit('pipelineClicked', this.$refs.linkedPipeline);
this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded); this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
}, },
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
onDownstreamHovered() { onDownstreamHovered() {
this.$emit('downstreamHovered', this.sourceJobName); this.$emit('downstreamHovered', this.sourceJobName);
}, },
...@@ -124,27 +130,23 @@ export default { ...@@ -124,27 +130,23 @@ export default {
<div <div
ref="linkedPipeline" ref="linkedPipeline"
v-gl-tooltip v-gl-tooltip
class="gl-downstream-pipeline-job-width" class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
:class="flexDirection"
:title="tooltipText" :title="tooltipText"
data-qa-selector="child_pipeline" data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered" @mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave" @mouseleave="onDownstreamHoverLeave"
> >
<div <div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1" <div class="gl-display-flex gl-pr-3">
:class="{ 'gl-pl-9': isUpstream }"
>
<div class="gl-display-flex gl-pr-7 gl-pipeline-job-width">
<ci-status <ci-status
v-if="!pipelineIsLoading" v-if="!pipelineIsLoading"
:status="pipelineStatus" :status="pipelineStatus"
:size="24" :size="24"
css-classes="gl-top-0 gl-pr-2" css-classes="gl-top-0 gl-pr-2"
/> />
<div v-else class="gl-pr-2"><gl-loading-icon size="sm" inline /></div> <div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
<div <div class="gl-display-flex gl-flex-direction-column gl-downstream-pipeline-job-width">
class="gl-display-flex gl-flex-direction-column gl-pipeline-job-width gl-text-truncate"
>
<span class="gl-text-truncate" data-testid="downstream-title"> <span class="gl-text-truncate" data-testid="downstream-title">
{{ downstreamTitle }} {{ downstreamTitle }}
</span> </span>
...@@ -160,10 +162,12 @@ export default { ...@@ -160,10 +162,12 @@ export default {
{{ label }} {{ label }}
</gl-badge> </gl-badge>
</div> </div>
</div>
<div class="gl-display-flex">
<gl-button <gl-button
:id="buttonId" :id="buttonId"
class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" class="gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`"
:icon="expandedIcon" :icon="expandedIcon"
:aria-label="__('Expand pipeline')" :aria-label="__('Expand pipeline')"
data-testid="expand-pipeline-button" data-testid="expand-pipeline-button"
......
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
} }
.gl-downstream-pipeline-job-width { .gl-downstream-pipeline-job-width {
width: 240px; width: 170px;
} }
.gl-linked-pipeline-padding { .gl-linked-pipeline-padding {
......
...@@ -9,6 +9,23 @@ import mockPipeline from './linked_pipelines_mock_data'; ...@@ -9,6 +9,23 @@ import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => { describe('Linked pipeline', () => {
let wrapper; let wrapper;
const downstreamProps = {
pipeline: {
...mockPipeline,
multiproject: false,
},
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
const upstreamProps = {
...downstreamProps,
columnTitle: 'Upstream',
type: UPSTREAM,
};
const findButton = () => wrapper.find(GlButton); const findButton = () => wrapper.find(GlButton);
const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]'); const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
...@@ -86,91 +103,65 @@ describe('Linked pipeline', () => { ...@@ -86,91 +103,65 @@ describe('Linked pipeline', () => {
}); });
}); });
describe('parent/child', () => { describe('upstream pipelines', () => {
const downstreamProps = { beforeEach(() => {
pipeline: { createWrapper(upstreamProps);
...mockPipeline, });
multiproject: false,
},
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
const upstreamProps = { it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
...downstreamProps, expect(findPipelineLabel().exists()).toBe(true);
columnTitle: 'Upstream', });
type: UPSTREAM,
};
it('parent/child label container should exist', () => { it('upstream pipeline should contain the correct link', () => {
expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path);
});
it('applies the reverse-row css class to the card', () => {
expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row-reverse');
expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row');
});
});
describe('downstream pipelines', () => {
beforeEach(() => {
createWrapper(downstreamProps); createWrapper(downstreamProps);
});
it('parent/child label container should exist', () => {
expect(findPipelineLabel().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);
expect(findPipelineLabel().exists()).toBe(true); expect(findPipelineLabel().exists()).toBe(true);
}); });
it('should have the name of the trigger job on the card when it is a child pipeline', () => { it('should have the name of the trigger job on the card when it is a child pipeline', () => {
createWrapper(downstreamProps);
expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name); expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
}); });
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
createWrapper(upstreamProps);
expect(findPipelineLabel().exists()).toBe(true);
});
it('downstream pipeline should contain the correct link', () => { it('downstream pipeline should contain the correct link', () => {
createWrapper(downstreamProps);
expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path); expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
}); });
it('upstream pipeline should contain the correct link', () => { it('applies the flex-row css class to the card', () => {
createWrapper(upstreamProps); expect(findLinkedPipeline().classes()).toContain('gl-flex-direction-row');
expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path); expect(findLinkedPipeline().classes()).not.toContain('gl-flex-direction-row-reverse');
}); });
});
describe('expand button', () => {
it.each` it.each`
presentClass | missingClass pipelineType | anglePosition | borderClass | expanded
${'gl-right-0'} | ${'gl-left-0'} ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-1!'} | ${false}
${'gl-border-l-1!'} | ${'gl-border-r-1!'} ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-1!'} | ${true}
`( ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-1!'} | ${false}
'pipeline expand button should be postioned right when child pipeline', ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-1!'} | ${true}
({ presentClass, missingClass }) => {
createWrapper(downstreamProps);
expect(findExpandButton().classes()).toContain(presentClass);
expect(findExpandButton().classes()).not.toContain(missingClass);
},
);
it.each`
presentClass | missingClass
${'gl-left-0'} | ${'gl-right-0'}
${'gl-border-r-1!'} | ${'gl-border-l-1!'}
`(
'pipeline expand button should be postioned left when parent pipeline',
({ presentClass, missingClass }) => {
createWrapper(upstreamProps);
expect(findExpandButton().classes()).toContain(presentClass);
expect(findExpandButton().classes()).not.toContain(missingClass);
},
);
it.each`
pipelineType | anglePosition | expanded
${downstreamProps} | ${'angle-right'} | ${false}
${downstreamProps} | ${'angle-left'} | ${true}
${upstreamProps} | ${'angle-left'} | ${false}
${upstreamProps} | ${'angle-right'} | ${true}
`( `(
'$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded', '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $borderClass if expanded state is $expanded',
({ pipelineType, anglePosition, expanded }) => { ({ pipelineType, anglePosition, borderClass, expanded }) => {
createWrapper({ ...pipelineType, expanded }); createWrapper({ ...pipelineType, expanded });
expect(findExpandButton().props('icon')).toBe(anglePosition); expect(findExpandButton().props('icon')).toBe(anglePosition);
expect(findExpandButton().classes()).toContain(borderClass);
}, },
); );
}); });
......
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