Commit 5afd00e1 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo Committed by Natalia Tepluhina

Add new sections and apply to component

Adds query and UI elements for new linked pipeline display
Some changes to existing components to accomodate
parent 1243e528
<script> <script>
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import { MAIN } from './constants'; import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
export default { export default {
name: 'PipelineGraph', name: 'PipelineGraph',
components: { components: {
LinkedGraphWrapper,
LinkedPipelinesColumn,
StageColumnComponent, StageColumnComponent,
}, },
props: { props: {
...@@ -23,10 +27,60 @@ export default { ...@@ -23,10 +27,60 @@ export default {
default: MAIN, default: MAIN,
}, },
}, },
pipelineTypeConstants: {
DOWNSTREAM,
UPSTREAM,
},
data() {
return {
hoveredJobName: '',
pipelineExpanded: {
jobName: '',
expanded: false,
},
};
},
computed: { computed: {
downstreamPipelines() {
return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
},
graph() { graph() {
return this.pipeline.stages; return this.pipeline.stages;
}, },
hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0);
},
hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0);
},
// The two show checks prevent upstream / downstream from showing redundant linked columns
showDownstreamPipelines() {
return (
this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
);
},
showUpstreamPipelines() {
return (
this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
);
},
upstreamPipelines() {
return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
},
methods: {
handleError(errorType) {
this.$emit('error', errorType);
},
setJob(jobName) {
this.hoveredJobName = jobName;
},
togglePipelineExpanded(jobName, expanded) {
this.pipelineExpanded = {
expanded,
jobName: expanded ? jobName : '',
};
},
}, },
}; };
</script> </script>
...@@ -36,13 +90,39 @@ export default { ...@@ -36,13 +90,39 @@ export default {
class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
:class="{ 'gl-py-5': !isLinkedPipeline }" :class="{ 'gl-py-5': !isLinkedPipeline }"
> >
<linked-graph-wrapper>
<template #upstream>
<linked-pipelines-column
v-if="showUpstreamPipelines"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
:type="$options.pipelineTypeConstants.UPSTREAM"
@error="handleError"
/>
</template>
<template #main>
<stage-column-component <stage-column-component
v-for="stage in graph" v-for="stage in graph"
:key="stage.name" :key="stage.name"
:title="stage.name" :title="stage.name"
:groups="stage.groups" :groups="stage.groups"
:action="stage.status.action" :action="stage.status.action"
:job-hovered="hoveredJobName"
:pipeline-expanded="pipelineExpanded"
/>
</template>
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
@downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded"
@error="handleError"
/> />
</template>
</linked-graph-wrapper>
</div> </div>
</div> </div>
</template> </template>
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
}; };
}, },
update(data) { update(data) {
return unwrapPipelineData(this.pipelineIid, data); return unwrapPipelineData(this.pipelineProjectPath, data);
}, },
error() { error() {
this.reportFailure(LOAD_FAILURE); this.reportFailure(LOAD_FAILURE);
...@@ -77,13 +77,11 @@ export default { ...@@ -77,13 +77,11 @@ export default {
}; };
</script> </script>
<template> <template>
<div>
<gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert"> <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
{{ alert.text }} {{ alert.text }}
</gl-alert> </gl-alert>
<gl-loading-icon <gl-loading-icon v-if="$apollo.queries.pipeline.loading" class="gl-mx-auto gl-my-4" size="lg" />
v-else-if="$apollo.queries.pipeline.loading" <pipeline-graph v-if="pipeline" :pipeline="pipeline" @error="reportFailure" />
class="gl-mx-auto gl-my-4" </div>
size="lg"
/>
<pipeline-graph v-else :pipeline="pipeline" />
</template> </template>
...@@ -25,23 +25,33 @@ export default { ...@@ -25,23 +25,33 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
pipeline: { expanded: {
type: Object, type: Boolean,
required: true, required: true,
}, },
projectId: { pipeline: {
type: Number, type: Object,
required: true, required: true,
}, },
type: { type: {
type: String, type: String,
required: true, required: true,
}, },
/*
The next two props will be removed or required
once the graph transition is done.
See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
*/
isLoading: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: -1,
}, },
data() {
return {
expanded: false,
};
}, },
computed: { computed: {
tooltipText() { tooltipText() {
...@@ -74,6 +84,9 @@ export default { ...@@ -74,6 +84,9 @@ export default {
} }
return __('Multi-project'); return __('Multi-project');
}, },
pipelineIsLoading() {
return Boolean(this.isLoading || this.pipeline.isLoading);
},
isDownstream() { isDownstream() {
return this.type === DOWNSTREAM; return this.type === DOWNSTREAM;
}, },
...@@ -81,7 +94,9 @@ export default { ...@@ -81,7 +94,9 @@ export default {
return this.type === UPSTREAM; return this.type === UPSTREAM;
}, },
isSameProject() { isSameProject() {
return this.projectId === this.pipeline.project.id; return this.projectId > -1
? this.projectId === this.pipeline.project.id
: !this.pipeline.multiproject;
}, },
sourceJobName() { sourceJobName() {
return accessValue(this.dataMethod, 'sourceJob', this.pipeline); return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
...@@ -101,16 +116,15 @@ export default { ...@@ -101,16 +116,15 @@ export default {
}, },
methods: { methods: {
onClickLinkedPipeline() { onClickLinkedPipeline() {
this.$root.$emit('bv::hide::tooltip', this.buttonId); this.hideTooltips();
this.expanded = !this.expanded;
this.$emit('pipelineClicked', this.$refs.linkedPipeline); this.$emit('pipelineClicked', this.$refs.linkedPipeline);
this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded); this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
}, },
hideTooltips() { hideTooltips() {
this.$root.$emit('bv::hide::tooltip'); this.$root.$emit('bv::hide::tooltip');
}, },
onDownstreamHovered() { onDownstreamHovered() {
this.$emit('downstreamHovered', this.pipeline.source_job.name); this.$emit('downstreamHovered', this.sourceJobName);
}, },
onDownstreamHoverLeave() { onDownstreamHoverLeave() {
this.$emit('downstreamHovered', ''); this.$emit('downstreamHovered', '');
...@@ -120,10 +134,10 @@ export default { ...@@ -120,10 +134,10 @@ export default {
</script> </script>
<template> <template>
<li <div
ref="linkedPipeline" ref="linkedPipeline"
v-gl-tooltip v-gl-tooltip
class="linked-pipeline build" class="linked-pipeline build gl-pipeline-job-width"
:title="tooltipText" :title="tooltipText"
:class="{ 'downstream-pipeline': isDownstream }" :class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline" data-qa-selector="child_pipeline"
...@@ -136,8 +150,9 @@ export default { ...@@ -136,8 +150,9 @@ export default {
> >
<div class="gl-display-flex"> <div class="gl-display-flex">
<ci-status <ci-status
v-if="!pipeline.isLoading" v-if="!pipelineIsLoading"
:status="pipelineStatus" :status="pipelineStatus"
: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 inline /></div> <div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
...@@ -160,10 +175,10 @@ export default { ...@@ -160,10 +175,10 @@ export default {
class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`"
:icon="expandedIcon" :icon="expandedIcon"
data-testid="expandPipelineButton" data-testid="expand-pipeline-button"
data-qa-selector="expand_pipeline_button" data-qa-selector="expand_pipeline_button"
@click="onClickLinkedPipeline" @click="onClickLinkedPipeline"
/> />
</div> </div>
</li> </div>
</template> </template>
<script> <script>
import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
import LinkedPipeline from './linked_pipeline.vue'; import LinkedPipeline from './linked_pipeline.vue';
import { LOAD_FAILURE } from '../../constants';
import { UPSTREAM } from './constants'; import { UPSTREAM } from './constants';
import { unwrapPipelineData } from './utils';
export default { export default {
components: { components: {
LinkedPipeline, LinkedPipeline,
PipelineGraph: () => import('./graph_component.vue'),
}, },
props: { props: {
columnTitle: { columnTitle: {
...@@ -19,11 +23,22 @@ export default { ...@@ -19,11 +23,22 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: Number,
required: true,
}, },
data() {
return {
currentPipeline: null,
loadingPipelineId: null,
pipelineExpanded: false,
};
}, },
titleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
'gl-pl-3',
'gl-mb-5',
],
computed: { computed: {
columnClass() { columnClass() {
const positionValues = { const positionValues = {
...@@ -35,14 +50,66 @@ export default { ...@@ -35,14 +50,66 @@ export default {
graphPosition() { graphPosition() {
return this.isUpstream ? 'left' : 'right'; return this.isUpstream ? 'left' : 'right';
}, },
// Refactor string match when BE returns Upstream/Downstream indicators
isUpstream() { isUpstream() {
return this.type === UPSTREAM; return this.type === UPSTREAM;
}, },
computedTitleClasses() {
const positionalClasses = this.isUpstream
? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
: [];
return [...this.$options.titleClasses, ...positionalClasses];
},
}, },
methods: { methods: {
onPipelineClick(downstreamNode, pipeline, index) { getPipelineData(pipeline) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode); const projectPath = pipeline.project.fullPath;
this.$apollo.addSmartQuery('currentPipeline', {
query: getPipelineDetails,
variables() {
return {
projectPath,
iid: pipeline.iid,
};
},
update(data) {
return unwrapPipelineData(projectPath, data);
},
result() {
this.loadingPipelineId = null;
},
error() {
this.$emit('error', LOAD_FAILURE);
},
});
},
isExpanded(id) {
return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id);
},
isLoadingPipeline(id) {
return this.loadingPipelineId === id;
},
onPipelineClick(pipeline) {
/* If the clicked pipeline has been expanded already, close it, clear, exit */
if (this.currentPipeline?.id === pipeline.id) {
this.pipelineExpanded = false;
this.currentPipeline = null;
return;
}
/* Set the loading id */
this.loadingPipelineId = pipeline.id;
/*
Expand the pipeline.
If this was not a toggle close action, and
it was already showing a different pipeline, then
this will be a no-op, but that doesn't matter.
*/
this.pipelineExpanded = true;
this.getPipelineData(pipeline);
}, },
onDownstreamHovered(jobName) { onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName); this.$emit('downstreamHovered', jobName);
...@@ -60,25 +127,40 @@ export default { ...@@ -60,25 +127,40 @@ export default {
</script> </script>
<template> <template>
<div :class="columnClass" class="stage-column linked-pipelines-column"> <div class="gl-display-flex">
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div> <div :class="columnClass" class="linked-pipelines-column">
<div v-if="isUpstream" class="cross-project-triangle"></div> <div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses">
<ul> {{ columnTitle }}
<linked-pipeline </div>
v-for="(pipeline, index) in linkedPipelines" <ul class="gl-pl-0">
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id" :key="pipeline.id"
:class="{ class="gl-display-flex gl-mb-4"
active: pipeline.isExpanded, :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
'left-connector': pipeline.isExpanded && graphPosition === 'left', >
}" <linked-pipeline
class="gl-display-inline-block"
:is-loading="isLoadingPipeline(pipeline.id)"
:pipeline="pipeline" :pipeline="pipeline"
:column-title="columnTitle" :column-title="columnTitle"
:project-id="projectId"
:type="type" :type="type"
@pipelineClicked="onPipelineClick($event, pipeline, index)" :expanded="isExpanded(pipeline.id)"
@downstreamHovered="onDownstreamHovered" @downstreamHovered="onDownstreamHovered"
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle" @pipelineExpandToggle="onPipelineExpandToggle"
/> />
<div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block">
<pipeline-graph
v-if="currentPipeline"
:type="type"
class="d-inline-block gl-mt-n2"
:pipeline="currentPipeline"
:is-linked-pipeline="true"
/>
</div>
</li>
</ul> </ul>
</div> </div>
</div>
</template> </template>
...@@ -35,7 +35,9 @@ export default { ...@@ -35,7 +35,9 @@ export default {
graphPosition() { graphPosition() {
return this.isUpstream ? 'left' : 'right'; return this.isUpstream ? 'left' : 'right';
}, },
// Refactor string match when BE returns Upstream/Downstream indicators isExpanded() {
return this.pipeline?.isExpanded || false;
},
isUpstream() { isUpstream() {
return this.type === UPSTREAM; return this.type === UPSTREAM;
}, },
...@@ -64,9 +66,8 @@ export default { ...@@ -64,9 +66,8 @@ export default {
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div> <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
<div v-if="isUpstream" class="cross-project-triangle"></div> <div v-if="isUpstream" class="cross-project-triangle"></div>
<ul> <ul>
<li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id">
<linked-pipeline <linked-pipeline
v-for="(pipeline, index) in linkedPipelines"
:key="pipeline.id"
:class="{ :class="{
active: pipeline.isExpanded, active: pipeline.isExpanded,
'left-connector': pipeline.isExpanded && graphPosition === 'left', 'left-connector': pipeline.isExpanded && graphPosition === 'left',
...@@ -75,10 +76,12 @@ export default { ...@@ -75,10 +76,12 @@ export default {
:column-title="columnTitle" :column-title="columnTitle"
:project-id="projectId" :project-id="projectId"
:type="type" :type="type"
:expanded="isExpanded"
@pipelineClicked="onPipelineClick($event, pipeline, index)" @pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered" @downstreamHovered="onDownstreamHovered"
@pipelineExpandToggle="onPipelineExpandToggle" @pipelineExpandToggle="onPipelineExpandToggle"
/> />
</li>
</ul> </ul>
</div> </div>
</template> </template>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { unwrapStagesWithNeeds } from '../unwrapping_utils'; import { unwrapStagesWithNeeds } from '../unwrapping_utils';
const addMulti = (mainId, pipeline) => { const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
return { ...pipeline, multiproject: mainId !== pipeline.id }; return {
...linkedPipeline,
multiproject: mainPipelineProjectPath !== linkedPipeline.project.fullPath,
};
}; };
const unwrapPipelineData = (mainPipelineId, data) => { const transformId = linkedPipeline => {
return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
};
const unwrapPipelineData = (mainPipelineProjectPath, data) => {
if (!data?.project?.pipeline) { if (!data?.project?.pipeline) {
return null; return null;
} }
const { pipeline } = data.project;
const { const {
id,
upstream, upstream,
downstream, downstream,
stages: { nodes: stages }, stages: { nodes: stages },
} = data.project.pipeline; } = pipeline;
const nodes = unwrapStagesWithNeeds(stages); const nodes = unwrapStagesWithNeeds(stages);
return { return {
id, ...pipeline,
id: getIdFromGraphQLId(pipeline.id),
stages: nodes, stages: nodes,
upstream: upstream ? [upstream].map(addMulti.bind(null, mainPipelineId)) : [], upstream: upstream
downstream: downstream ? downstream.map(addMulti.bind(null, mainPipelineId)) : [], ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
: [],
downstream: downstream
? downstream.nodes.map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
: [],
}; };
}; };
......
<template>
<div class="gl-display-flex">
<slot name="upstream"></slot>
<slot name="main"></slot>
<slot name="downstream"></slot>
</div>
</template>
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
<template> <template>
<div> <div>
<div <div
class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-py-4 gl-mb-5" class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-mb-5"
:class="stageClasses" :class="stageClasses"
> >
<slot name="stages"> </slot> <slot name="stages"> </slot>
......
fragment LinkedPipelineData on Pipeline {
id
iid
path
status: detailedStatus {
group
label
icon
}
sourceJob {
name
}
project {
name
fullPath
}
}
#import "../fragments/linked_pipelines.fragment.graphql"
query getPipelineDetails($projectPath: ID!, $iid: ID!) { query getPipelineDetails($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
pipeline(iid: $iid) { pipeline(iid: $iid) {
id: iid id
iid
downstream {
nodes {
...LinkedPipelineData
}
}
upstream {
...LinkedPipelineData
}
stages { stages {
nodes { nodes {
name name
......
...@@ -2,6 +2,7 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) { ...@@ -2,6 +2,7 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
pipeline(iid: $iid) { pipeline(iid: $iid) {
id id
iid
status status
retryable retryable
cancelable cancelable
......
...@@ -139,6 +139,10 @@ ...@@ -139,6 +139,10 @@
width: 186px; width: 186px;
} }
.gl-linked-pipeline-padding {
padding-right: 120px;
}
.gl-build-content { .gl-build-content {
@include build-content(); @include build-content();
} }
......
...@@ -15,8 +15,8 @@ describe('graph component', () => { ...@@ -15,8 +15,8 @@ describe('graph component', () => {
let mediator; let mediator;
let wrapper; let wrapper;
const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]'); const findExpandPipelineBtn = () => wrapper.find('[data-testid="expand-pipeline-button"]');
const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]'); const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expand-pipeline-button"]');
const findStageColumns = () => wrapper.findAll(StageColumnComponentLegacy); const findStageColumns = () => wrapper.findAll(StageColumnComponentLegacy);
const findStageColumnAt = i => findStageColumns().at(i); const findStageColumnAt = i => findStageColumns().at(i);
......
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import { unwrapPipelineData } from '~/pipelines/components/graph/utils'; import { GRAPHQL } from '~/pipelines/components/graph/constants';
import { mockPipelineResponse } from './mock_data'; import {
generateResponse,
mockPipelineResponse,
pipelineWithUpstreamDownstream,
} from './mock_data';
describe('graph component', () => { describe('graph component', () => {
let wrapper; let wrapper;
...@@ -11,10 +15,8 @@ describe('graph component', () => { ...@@ -11,10 +15,8 @@ describe('graph component', () => {
const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn); const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn);
const findStageColumns = () => wrapper.findAll(StageColumnComponent); const findStageColumns = () => wrapper.findAll(StageColumnComponent);
const generateResponse = raw => unwrapPipelineData(raw.data.project.pipeline.id, raw.data);
const defaultProps = { const defaultProps = {
pipeline: generateResponse(mockPipelineResponse), pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
}; };
const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => {
...@@ -23,6 +25,9 @@ describe('graph component', () => { ...@@ -23,6 +25,9 @@ describe('graph component', () => {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
provide: {
dataMethod: GRAPHQL,
},
}); });
}; };
...@@ -33,7 +38,7 @@ describe('graph component', () => { ...@@ -33,7 +38,7 @@ describe('graph component', () => {
describe('with data', () => { describe('with data', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ mountFn: mount });
}); });
it('renders the main columns in the graph', () => { it('renders the main columns in the graph', () => {
...@@ -43,11 +48,24 @@ describe('graph component', () => { ...@@ -43,11 +48,24 @@ describe('graph component', () => {
describe('when linked pipelines are not present', () => { describe('when linked pipelines are not present', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ mountFn: mount });
}); });
it('should not render a linked pipelines column', () => { it('should not render a linked pipelines column', () => {
expect(findLinkedColumns()).toHaveLength(0); expect(findLinkedColumns()).toHaveLength(0);
}); });
}); });
describe('when linked pipelines are present', () => {
beforeEach(() => {
createComponent({
mountFn: mount,
props: { pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse) },
});
});
it('should render linked pipelines columns', () => {
expect(findLinkedColumns()).toHaveLength(2);
});
});
}); });
...@@ -17,7 +17,7 @@ describe('Linked pipeline', () => { ...@@ -17,7 +17,7 @@ describe('Linked pipeline', () => {
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]'); const findPipelineLink = () => wrapper.find('[data-testid="pipelineLink"]');
const findExpandButton = () => wrapper.find('[data-testid="expandPipelineButton"]'); const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
const createWrapper = (propsData, data = []) => { const createWrapper = (propsData, data = []) => {
wrapper = mount(LinkedPipelineComponent, { wrapper = mount(LinkedPipelineComponent, {
...@@ -40,20 +40,13 @@ describe('Linked pipeline', () => { ...@@ -40,20 +40,13 @@ describe('Linked pipeline', () => {
projectId: invalidTriggeredPipelineId, projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream', columnTitle: 'Downstream',
type: DOWNSTREAM, type: DOWNSTREAM,
expanded: false,
}; };
beforeEach(() => { beforeEach(() => {
createWrapper(props); createWrapper(props);
}); });
it('should render a list item as the containing element', () => {
expect(wrapper.element.tagName).toBe('LI');
});
it('should render a button', () => {
expect(findButton().exists()).toBe(true);
});
it('should render the project name', () => { it('should render the project name', () => {
expect(wrapper.text()).toContain(props.pipeline.project.name); expect(wrapper.text()).toContain(props.pipeline.project.name);
}); });
...@@ -105,12 +98,14 @@ describe('Linked pipeline', () => { ...@@ -105,12 +98,14 @@ describe('Linked pipeline', () => {
projectId: validTriggeredPipelineId, projectId: validTriggeredPipelineId,
columnTitle: 'Downstream', columnTitle: 'Downstream',
type: DOWNSTREAM, type: DOWNSTREAM,
expanded: false,
}; };
const upstreamProps = { const upstreamProps = {
...downstreamProps, ...downstreamProps,
columnTitle: 'Upstream', columnTitle: 'Upstream',
type: UPSTREAM, type: UPSTREAM,
expanded: false,
}; };
it('parent/child label container should exist', () => { it('parent/child label container should exist', () => {
...@@ -173,7 +168,7 @@ describe('Linked pipeline', () => { ...@@ -173,7 +168,7 @@ describe('Linked pipeline', () => {
`( `(
'$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded', '$pipelineType.columnTitle pipeline button icon should be $anglePosition if expanded state is $expanded',
({ pipelineType, anglePosition, expanded }) => { ({ pipelineType, anglePosition, expanded }) => {
createWrapper(pipelineType, { expanded }); createWrapper({ ...pipelineType, expanded });
expect(findExpandButton().props('icon')).toBe(anglePosition); expect(findExpandButton().props('icon')).toBe(anglePosition);
}, },
); );
...@@ -185,6 +180,7 @@ describe('Linked pipeline', () => { ...@@ -185,6 +180,7 @@ describe('Linked pipeline', () => {
projectId: invalidTriggeredPipelineId, projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream', columnTitle: 'Downstream',
type: DOWNSTREAM, type: DOWNSTREAM,
expanded: false,
}; };
beforeEach(() => { beforeEach(() => {
...@@ -202,6 +198,7 @@ describe('Linked pipeline', () => { ...@@ -202,6 +198,7 @@ describe('Linked pipeline', () => {
projectId: validTriggeredPipelineId, projectId: validTriggeredPipelineId,
columnTitle: 'Downstream', columnTitle: 'Downstream',
type: DOWNSTREAM, type: DOWNSTREAM,
expanded: false,
}; };
beforeEach(() => { beforeEach(() => {
...@@ -219,10 +216,7 @@ describe('Linked pipeline', () => { ...@@ -219,10 +216,7 @@ describe('Linked pipeline', () => {
jest.spyOn(wrapper.vm.$root, '$emit'); jest.spyOn(wrapper.vm.$root, '$emit');
findButton().trigger('click'); findButton().trigger('click');
expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([ expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual(['bv::hide::tooltip']);
'bv::hide::tooltip',
'js-linked-pipeline-34993051',
]);
}); });
it('should emit downstreamHovered with job name on mouseover', () => { it('should emit downstreamHovered with job name on mouseover', () => {
......
import { shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue'; import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import { UPSTREAM } from '~/pipelines/components/graph/constants'; import getPipelineDetails from '~/pipelines/graphql/queries/get_pipeline_details.query.graphql';
import mockData from './linked_pipelines_mock_data'; import { DOWNSTREAM, GRAPHQL } from '~/pipelines/components/graph/constants';
import { LOAD_FAILURE } from '~/pipelines/constants';
import {
mockPipelineResponse,
pipelineWithUpstreamDownstream,
wrappedPipelineReturn,
} from './mock_data';
const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
describe('Linked Pipelines Column', () => { describe('Linked Pipelines Column', () => {
const propsData = { const defaultProps = {
columnTitle: 'Upstream', columnTitle: 'Upstream',
linkedPipelines: mockData.triggered, linkedPipelines: processedPipeline.downstream,
graphPosition: 'right', type: DOWNSTREAM,
projectId: 19,
type: UPSTREAM,
}; };
let wrapper; let wrapper;
const findLinkedColumnTitle = () => wrapper.find('[data-testid="linked-column-title"]');
const findLinkedPipelineElements = () => wrapper.findAll(LinkedPipeline);
const findPipelineGraph = () => wrapper.find(PipelineGraph);
const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]');
beforeEach(() => { const localVue = createLocalVue();
wrapper = shallowMount(LinkedPipelinesColumn, { propsData }); localVue.use(VueApollo);
const createComponent = ({ apolloProvider, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(LinkedPipelinesColumn, {
apolloProvider,
localVue,
propsData: {
...defaultProps,
...props,
},
provide: {
dataMethod: GRAPHQL,
},
}); });
};
const createComponentWithApollo = (
mountFn = shallowMount,
getPipelineDetailsHandler = jest.fn().mockResolvedValue(wrappedPipelineReturn),
) => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
createComponent({ apolloProvider, mountFn });
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('renders the pipeline orientation', () => { describe('it renders correctly', () => {
const titleElement = wrapper.find('.linked-pipelines-column-title'); beforeEach(() => {
createComponent();
});
expect(titleElement.text()).toBe(propsData.columnTitle); it('renders the pipeline title', () => {
expect(findLinkedColumnTitle().text()).toBe(defaultProps.columnTitle);
}); });
it('renders the correct number of linked pipelines', () => { it('renders the correct number of linked pipelines', () => {
const linkedPipelineElements = wrapper.findAll(LinkedPipeline); expect(findLinkedPipelineElements()).toHaveLength(defaultProps.linkedPipelines.length);
});
});
describe('click action', () => {
const clickExpandButton = async () => {
await findExpandButton().trigger('click');
await wrapper.vm.$nextTick();
};
expect(linkedPipelineElements.length).toBe(propsData.linkedPipelines.length); const clickExpandButtonAndAwaitTimers = async () => {
await clickExpandButton();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
};
describe('when successful', () => {
beforeEach(() => {
createComponentWithApollo(mount);
}); });
it('renders cross project triangle when column is upstream', () => { it('toggles the pipeline visibility', async () => {
expect(wrapper.find('.cross-project-triangle').exists()).toBe(true); expect(findPipelineGraph().exists()).toBe(false);
await clickExpandButtonAndAwaitTimers();
expect(findPipelineGraph().exists()).toBe(true);
await clickExpandButton();
expect(findPipelineGraph().exists()).toBe(false);
});
});
describe('on error', () => {
beforeEach(() => {
createComponentWithApollo(mount, jest.fn().mockRejectedValue(new Error('GraphQL error')));
});
it('emits the error', async () => {
await clickExpandButton();
expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]);
});
it('does not show the pipeline', async () => {
expect(findPipelineGraph().exists()).toBe(false);
await clickExpandButtonAndAwaitTimers();
expect(findPipelineGraph().exists()).toBe(false);
});
});
}); });
}); });
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
export const mockPipelineResponse = { export const mockPipelineResponse = {
data: { data: {
project: { project: {
__typename: 'Project', __typename: 'Project',
pipeline: { pipeline: {
__typename: 'Pipeline', __typename: 'Pipeline',
id: '22', id: 163,
iid: '22',
downstream: null,
upstream: null,
stages: { stages: {
__typename: 'CiStageConnection', __typename: 'CiStageConnection',
nodes: [ nodes: [
...@@ -497,3 +502,164 @@ export const mockPipelineResponse = { ...@@ -497,3 +502,164 @@ export const mockPipelineResponse = {
}, },
}, },
}; };
export const downstream = {
nodes: [
{
id: 175,
iid: '31',
path: '/root/elemenohpee/-/pipelines/175',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
__typename: 'DetailedStatus',
},
sourceJob: {
name: 'test_c',
__typename: 'CiJob',
},
project: {
id: 'gid://gitlab/Project/25',
name: 'elemenohpee',
fullPath: 'root/elemenohpee',
__typename: 'Project',
},
__typename: 'Pipeline',
multiproject: true,
},
{
id: 181,
iid: '27',
path: '/root/abcd-dag/-/pipelines/181',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
__typename: 'DetailedStatus',
},
sourceJob: {
name: 'test_d',
__typename: 'CiJob',
},
project: {
id: 'gid://gitlab/Project/23',
name: 'abcd-dag',
fullPath: 'root/abcd-dag',
__typename: 'Project',
},
__typename: 'Pipeline',
multiproject: false,
},
],
};
export const upstream = {
id: 161,
iid: '24',
path: '/root/abcd-dag/-/pipelines/161',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
__typename: 'DetailedStatus',
},
sourceJob: null,
project: {
id: 'gid://gitlab/Project/23',
name: 'abcd-dag',
fullPath: 'root/abcd-dag',
__typename: 'Project',
},
__typename: 'Pipeline',
multiproject: true,
};
export const wrappedPipelineReturn = {
data: {
project: {
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38',
downstream: {
nodes: [],
},
upstream: {
id: 'gid://gitlab/Ci::Pipeline/174',
iid: '37',
path: '/root/elemenohpee/-/pipelines/174',
status: {
group: 'success',
label: 'passed',
icon: 'status_success',
},
sourceJob: {
name: 'test_c',
},
project: {
id: 'gid://gitlab/Project/25',
name: 'elemenohpee',
fullPath: 'root/elemenohpee',
},
},
stages: {
nodes: [
{
name: 'build',
status: {
action: null,
},
groups: {
nodes: [
{
status: {
label: 'passed',
group: 'success',
icon: 'status_success',
},
name: 'build_n',
size: 1,
jobs: {
nodes: [
{
name: 'build_n',
scheduledAt: null,
needs: {
nodes: [],
},
status: {
icon: 'status_success',
tooltip: 'passed',
hasDetails: true,
detailsPath: '/root/elemenohpee/-/jobs/1662',
group: 'success',
action: {
buttonTitle: 'Retry this job',
icon: 'retry',
path: '/root/elemenohpee/-/jobs/1662/retry',
title: 'Retry',
},
},
},
],
},
},
],
},
},
],
},
},
},
},
};
export const generateResponse = (raw, mockPath) => unwrapPipelineData(mockPath, raw.data);
export const pipelineWithUpstreamDownstream = base => {
const pip = { ...base };
pip.data.project.pipeline.downstream = downstream;
pip.data.project.pipeline.upstream = upstream;
return generateResponse(pip, 'root/abcd-dag');
};
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