Commit dcaf929b authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '298930-add-needs-view' into 'master'

Pipeline Graph: Add ability to switch between views

See merge request gitlab-org/gitlab!58646
parents 1b728a48 05f0211d
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinksLayer from '../graph_shared/links_layer.vue'; import LinksLayer from '../graph_shared/links_layer.vue';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH } from './constants'; import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants';
import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import { validateConfigPaths } from './utils'; import { validateConfigPaths } from './utils';
...@@ -25,11 +25,20 @@ export default { ...@@ -25,11 +25,20 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
viewType: {
type: String,
required: true,
},
isLinkedPipeline: { isLinkedPipeline: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
pipelineLayers: {
type: Array,
required: false,
default: () => [],
},
type: { type: {
type: String, type: String,
required: false, required: false,
...@@ -63,8 +72,8 @@ export default { ...@@ -63,8 +72,8 @@ export default {
downstreamPipelines() { downstreamPipelines() {
return this.hasDownstreamPipelines ? this.pipeline.downstream : []; return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
}, },
graph() { layout() {
return this.pipeline.stages; return this.isStageView ? this.pipeline.stages : this.generateColumnsFromLayersList();
}, },
hasDownstreamPipelines() { hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0); return Boolean(this.pipeline?.downstream?.length > 0);
...@@ -72,12 +81,18 @@ export default { ...@@ -72,12 +81,18 @@ export default {
hasUpstreamPipelines() { hasUpstreamPipelines() {
return Boolean(this.pipeline?.upstream?.length > 0); return Boolean(this.pipeline?.upstream?.length > 0);
}, },
isStageView() {
return this.viewType === STAGE_VIEW;
},
metricsConfig() { metricsConfig() {
return { return {
path: this.configPaths.metricsPath, path: this.configPaths.metricsPath,
collectMetrics: true, collectMetrics: true,
}; };
}, },
shouldHideLinks() {
return this.isStageView;
},
// The show downstream check prevents showing redundant linked columns // The show downstream check prevents showing redundant linked columns
showDownstreamPipelines() { showDownstreamPipelines() {
return ( return (
...@@ -101,6 +116,26 @@ export default { ...@@ -101,6 +116,26 @@ export default {
this.getMeasurements(); this.getMeasurements();
}, },
methods: { methods: {
generateColumnsFromLayersList() {
return this.pipelineLayers.map((layers, idx) => {
/*
look up the groups in each layer,
then add each set of layer groups to a stage-like object
*/
const groups = layers.map((id) => {
const { stageIdx, groupIdx } = this.pipeline.stagesLookup[id];
return this.pipeline.stages?.[stageIdx]?.groups?.[groupIdx];
});
return {
name: '',
id: `layer-${idx}`,
status: { action: null },
groups: groups.filter(Boolean),
};
});
},
getMeasurements() { getMeasurements() {
this.measurements = { this.measurements = {
width: this.$refs[this.containerId].scrollWidth, width: this.$refs[this.containerId].scrollWidth,
...@@ -147,29 +182,31 @@ export default { ...@@ -147,29 +182,31 @@ export default {
:linked-pipelines="upstreamPipelines" :linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')" :column-title="__('Upstream')"
:type="$options.pipelineTypeConstants.UPSTREAM" :type="$options.pipelineTypeConstants.UPSTREAM"
:view-type="viewType"
@error="onError" @error="onError"
/> />
</template> </template>
<template #main> <template #main>
<div :id="containerId" :ref="containerId"> <div :id="containerId" :ref="containerId">
<links-layer <links-layer
:pipeline-data="graph" :pipeline-data="layout"
:pipeline-id="pipeline.id" :pipeline-id="pipeline.id"
:container-id="containerId" :container-id="containerId"
:container-measurements="measurements" :container-measurements="measurements"
:highlighted-job="hoveredJobName" :highlighted-job="hoveredJobName"
:metrics-config="metricsConfig" :metrics-config="metricsConfig"
:never-show-links="true" :never-show-links="shouldHideLinks"
:view-type="viewType"
default-link-color="gl-stroke-transparent" default-link-color="gl-stroke-transparent"
@error="onError" @error="onError"
@highlightedJobsChange="updateHighlightedJobs" @highlightedJobsChange="updateHighlightedJobs"
> >
<stage-column-component <stage-column-component
v-for="stage in graph" v-for="column in layout"
:key="stage.name" :key="column.id || column.name"
:title="stage.name" :title="column.name"
:groups="stage.groups" :groups="column.groups"
:action="stage.status.action" :action="column.status.action"
:highlighted-jobs="highlightedJobs" :highlighted-jobs="highlightedJobs"
:job-hovered="hoveredJobName" :job-hovered="hoveredJobName"
:pipeline-expanded="pipelineExpanded" :pipeline-expanded="pipelineExpanded"
...@@ -189,6 +226,7 @@ export default { ...@@ -189,6 +226,7 @@ export default {
:linked-pipelines="downstreamPipelines" :linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')" :column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM" :type="$options.pipelineTypeConstants.DOWNSTREAM"
:view-type="viewType"
@downstreamHovered="setJob" @downstreamHovered="setJob"
@pipelineExpandToggle="togglePipelineExpanded" @pipelineExpandToggle="togglePipelineExpanded"
@scrollContainer="slidePipelineContainer" @scrollContainer="slidePipelineContainer"
......
...@@ -5,7 +5,8 @@ import { __ } from '~/locale'; ...@@ -5,7 +5,8 @@ import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants'; import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import { IID_FAILURE, STAGE_VIEW } from './constants'; import { listByLayers } from '../parsing_utils';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW } from './constants';
import PipelineGraph from './graph_component.vue'; import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue'; import GraphViewSelector from './graph_view_selector.vue';
import { import {
...@@ -43,6 +44,7 @@ export default { ...@@ -43,6 +44,7 @@ export default {
alertType: null, alertType: null,
currentViewType: STAGE_VIEW, currentViewType: STAGE_VIEW,
pipeline: null, pipeline: null,
pipelineLayers: null,
showAlert: false, showAlert: false,
}; };
}, },
...@@ -155,6 +157,13 @@ export default { ...@@ -155,6 +157,13 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
}, },
methods: { methods: {
getPipelineLayers() {
if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) {
this.pipelineLayers = listByLayers(this.pipeline);
}
return this.pipelineLayers;
},
hideAlert() { hideAlert() {
this.showAlert = false; this.showAlert = false;
this.alertType = null; this.alertType = null;
...@@ -192,6 +201,8 @@ export default { ...@@ -192,6 +201,8 @@ export default {
v-if="pipeline" v-if="pipeline"
:config-paths="configPaths" :config-paths="configPaths"
:pipeline="pipeline" :pipeline="pipeline"
:pipeline-layers="getPipelineLayers()"
:view-type="currentViewType"
@error="reportFailure" @error="reportFailure"
@refreshPipelineGraph="refreshPipelineGraph" @refreshPipelineGraph="refreshPipelineGraph"
/> />
......
...@@ -55,16 +55,16 @@ export default { ...@@ -55,16 +55,16 @@ export default {
</script> </script>
<template> <template>
<div class="gl-display-flex gl-justify-content-end gl-align-items-center gl-my-4"> <div class="gl-display-flex gl-align-items-center gl-my-4">
<span>{{ $options.i18n.labelText }}</span> <span>{{ $options.i18n.labelText }}</span>
<gl-dropdown class="gl-ml-4" :right="true"> <gl-dropdown class="gl-ml-4">
<template #button-content> <template #button-content>
<gl-sprintf :message="currentDropdownText"> <gl-sprintf :message="currentDropdownText">
<template #code="{ content }"> <template #code="{ content }">
<code> {{ content }} </code> <code> {{ content }} </code>
</template> </template>
</gl-sprintf> </gl-sprintf>
<gl-icon class="gl-px-2" name="angle-down" :size="18" /> <gl-icon class="gl-px-2" name="angle-down" :size="16" />
</template> </template>
<gl-dropdown-item <gl-dropdown-item
v-for="view in $options.views" v-for="view in $options.views"
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { LOAD_FAILURE } from '../../constants'; import { LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import { ONE_COL_WIDTH, UPSTREAM } from './constants'; import { listByLayers } from '../parsing_utils';
import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW } from './constants';
import LinkedPipeline from './linked_pipeline.vue'; import LinkedPipeline from './linked_pipeline.vue';
import { import {
getQueryHeaders, getQueryHeaders,
...@@ -35,11 +36,16 @@ export default { ...@@ -35,11 +36,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
viewType: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
currentPipeline: null, currentPipeline: null,
loadingPipelineId: null, loadingPipelineId: null,
pipelineLayers: {},
pipelineExpanded: false, pipelineExpanded: false,
}; };
}, },
...@@ -123,6 +129,13 @@ export default { ...@@ -123,6 +129,13 @@ export default {
toggleQueryPollingByVisibility(this.$apollo.queries.currentPipeline); toggleQueryPollingByVisibility(this.$apollo.queries.currentPipeline);
}, },
getPipelineLayers(id) {
if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) {
this.pipelineLayers[id] = listByLayers(this.currentPipeline);
}
return this.pipelineLayers[id];
},
isExpanded(id) { isExpanded(id) {
return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id); return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id);
}, },
...@@ -203,7 +216,9 @@ export default { ...@@ -203,7 +216,9 @@ export default {
class="d-inline-block gl-mt-n2" class="d-inline-block gl-mt-n2"
:config-paths="configPaths" :config-paths="configPaths"
:pipeline="currentPipeline" :pipeline="currentPipeline"
:pipeline-layers="getPipelineLayers(pipeline.id)"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:view-type="viewType"
/> />
</div> </div>
</li> </li>
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { unwrapStagesWithNeeds } from '../unwrapping_utils'; import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
const addMulti = (mainPipelineProjectPath, linkedPipeline) => { const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
return { return {
...@@ -86,12 +86,13 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { ...@@ -86,12 +86,13 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
stages: { nodes: stages }, stages: { nodes: stages },
} = pipeline; } = pipeline;
const nodes = unwrapStagesWithNeeds(stages); const { stages: updatedStages, lookup } = unwrapStagesWithNeedsAndLookup(stages);
return { return {
...pipeline, ...pipeline,
id: getIdFromGraphQLId(pipeline.id), id: getIdFromGraphQLId(pipeline.id),
stages: nodes, stages: updatedStages,
stagesLookup: lookup,
upstream: upstream upstream: upstream
? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId) ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
: [], : [],
......
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
import { performanceMarkAndMeasure } from '~/performance/utils'; import { performanceMarkAndMeasure } from '~/performance/utils';
import { DRAW_FAILURE } from '../../constants'; import { DRAW_FAILURE } from '../../constants';
import { createJobsHash, generateJobNeedsDict, reportToSentry } from '../../utils'; import { createJobsHash, generateJobNeedsDict, reportToSentry } from '../../utils';
import { STAGE_VIEW } from '../graph/constants';
import { parseData } from '../parsing_utils'; import { parseData } from '../parsing_utils';
import { reportPerformance } from './api'; import { reportPerformance } from './api';
import { generateLinksData } from './drawing_utils'; import { generateLinksData } from './drawing_utils';
...@@ -54,11 +55,17 @@ export default { ...@@ -54,11 +55,17 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
viewType: {
type: String,
required: false,
default: STAGE_VIEW,
},
}, },
data() { data() {
return { return {
links: [], links: [],
needsObject: null, needsObject: null,
parsedData: {},
}; };
}, },
computed: { computed: {
...@@ -108,6 +115,15 @@ export default { ...@@ -108,6 +115,15 @@ export default {
highlightedJobs(jobs) { highlightedJobs(jobs) {
this.$emit('highlightedJobsChange', jobs); this.$emit('highlightedJobsChange', jobs);
}, },
viewType() {
/*
We need to wait a tick so that the layout reflows
before the links refresh.
*/
this.$nextTick(() => {
this.refreshLinks();
});
},
}, },
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
...@@ -166,14 +182,17 @@ export default { ...@@ -166,14 +182,17 @@ export default {
this.beginPerfMeasure(); this.beginPerfMeasure();
try { try {
const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups); const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs); this.parsedData = parseData(arrayOfJobs);
this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`); this.refreshLinks();
} catch (err) { } catch (err) {
this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false }); this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
reportToSentry(this.$options.name, err); reportToSentry(this.$options.name, err);
} }
this.finishPerfMeasureAndSend(); this.finishPerfMeasureAndSend();
}, },
refreshLinks() {
this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`);
},
getLinkClasses(link) { getLinkClasses(link) {
return [ return [
this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor, this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor,
......
import { uniqWith, isEqual } from 'lodash'; import { uniqWith, isEqual } from 'lodash';
import { createSankey } from './dag/drawing_utils';
/* /*
The following functions are the main engine in transforming the data as The following functions are the main engine in transforming the data as
...@@ -144,3 +145,28 @@ export const getMaxNodes = (nodes) => { ...@@ -144,3 +145,28 @@ export const getMaxNodes = (nodes) => {
export const removeOrphanNodes = (sankeyfiedNodes) => { export const removeOrphanNodes = (sankeyfiedNodes) => {
return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length); return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length);
}; };
/*
This utility accepts unwrapped pipeline data in the format returned from
our standard pipeline GraphQL query and returns a list of names by layer
for the layer view. It can be combined with the stageLookup on the pipeline
to generate columns by layer.
*/
export const listByLayers = ({ stages }) => {
const arrayOfJobs = stages.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs);
const dataWithLayers = createSankey()(parsedData);
return dataWithLayers.nodes.reduce((acc, { layer, name }) => {
/* sort groups by layer */
if (!acc[layer]) {
acc[layer] = [];
}
acc[layer].push(name);
return acc;
}, []);
};
import { reportToSentry } from '../utils'; import { reportToSentry } from '../utils';
const unwrapGroups = (stages) => { const unwrapGroups = (stages) => {
return stages.map((stage) => { return stages.map((stage, idx) => {
const { const {
groups: { nodes: groups }, groups: { nodes: groups },
} = stage; } = stage;
return { ...stage, groups }; return { node: { ...stage, groups }, lookup: { stageIdx: idx } };
}); });
}; };
...@@ -23,20 +23,34 @@ const unwrapJobWithNeeds = (denodedJobArray) => { ...@@ -23,20 +23,34 @@ const unwrapJobWithNeeds = (denodedJobArray) => {
return unwrapNodesWithName(denodedJobArray, 'needs'); return unwrapNodesWithName(denodedJobArray, 'needs');
}; };
const unwrapStagesWithNeeds = (denodedStages) => { const unwrapStagesWithNeedsAndLookup = (denodedStages) => {
const unwrappedNestedGroups = unwrapGroups(denodedStages); const unwrappedNestedGroups = unwrapGroups(denodedStages);
const nodes = unwrappedNestedGroups.map((node) => { const lookupMap = {};
const nodes = unwrappedNestedGroups.map(({ node, lookup }) => {
const { groups } = node; const { groups } = node;
const groupsWithJobs = groups.map((group) => { const groupsWithJobs = groups.map((group, idx) => {
const jobs = unwrapJobWithNeeds(group.jobs.nodes); const jobs = unwrapJobWithNeeds(group.jobs.nodes);
lookupMap[group.name] = { ...lookup, groupIdx: idx };
return { ...group, jobs }; return { ...group, jobs };
}); });
return { ...node, groups: groupsWithJobs }; return { ...node, groups: groupsWithJobs };
}); });
return nodes; return { stages: nodes, lookup: lookupMap };
}; };
export { unwrapGroups, unwrapNodesWithName, unwrapJobWithNeeds, unwrapStagesWithNeeds }; const unwrapStagesWithNeeds = (denodedStages) => {
return unwrapStagesWithNeedsAndLookup(denodedStages).stages;
};
export {
unwrapGroups,
unwrapJobWithNeeds,
unwrapNodesWithName,
unwrapStagesWithNeeds,
unwrapStagesWithNeedsAndLookup,
};
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { GRAPHQL } from '~/pipelines/components/graph/constants'; import { GRAPHQL, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue'; import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
...@@ -20,6 +20,7 @@ describe('graph component', () => { ...@@ -20,6 +20,7 @@ describe('graph component', () => {
const defaultProps = { const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'), pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
viewType: STAGE_VIEW,
configPaths: { configPaths: {
metricsPath: '', metricsPath: '',
graphqlResourceEtag: 'this/is/a/path', graphqlResourceEtag: 'this/is/a/path',
......
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { IID_FAILURE } from '~/pipelines/components/graph/constants'; import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue'; import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue'; import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import { mockPipelineResponse } from './mock_data'; import { mockPipelineResponse } from './mock_data';
const defaultProvide = { const defaultProvide = {
...@@ -24,6 +26,9 @@ describe('Pipeline graph wrapper', () => { ...@@ -24,6 +26,9 @@ describe('Pipeline graph wrapper', () => {
const getAlert = () => wrapper.find(GlAlert); const getAlert = () => wrapper.find(GlAlert);
const getLoadingIcon = () => wrapper.find(GlLoadingIcon); const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getGraph = () => wrapper.find(PipelineGraph); const getGraph = () => wrapper.find(PipelineGraph);
const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const getAllStageColumnGroupsInColumn = () =>
wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
const getViewSelector = () => wrapper.find(GraphViewSelector); const getViewSelector = () => wrapper.find(GraphViewSelector);
const createComponent = ({ const createComponent = ({
...@@ -48,12 +53,13 @@ describe('Pipeline graph wrapper', () => { ...@@ -48,12 +53,13 @@ describe('Pipeline graph wrapper', () => {
const createComponentWithApollo = ({ const createComponentWithApollo = ({
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse), getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
mountFn = shallowMount,
provide = {}, provide = {},
} = {}) => { } = {}) => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]]; const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers); const apolloProvider = createMockApollo(requestHandlers);
createComponent({ apolloProvider, provide }); createComponent({ apolloProvider, provide, mountFn });
}; };
afterEach(() => { afterEach(() => {
...@@ -223,13 +229,16 @@ describe('Pipeline graph wrapper', () => { ...@@ -223,13 +229,16 @@ describe('Pipeline graph wrapper', () => {
}); });
describe('when feature flag is on', () => { describe('when feature flag is on', () => {
let layersFn;
beforeEach(async () => { beforeEach(async () => {
layersFn = jest.spyOn(parsingUtils, 'listByLayers');
createComponentWithApollo({ createComponentWithApollo({
provide: { provide: {
glFeatures: { glFeatures: {
pipelineGraphLayersView: true, pipelineGraphLayersView: true,
}, },
}, },
mountFn: mount,
}); });
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
...@@ -239,6 +248,26 @@ describe('Pipeline graph wrapper', () => { ...@@ -239,6 +248,26 @@ describe('Pipeline graph wrapper', () => {
it('appears', () => { it('appears', () => {
expect(getViewSelector().exists()).toBe(true); expect(getViewSelector().exists()).toBe(true);
}); });
it('switches between views', async () => {
const groupsInFirstColumn =
mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes.length;
expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
expect(getStageColumnTitle().text()).toBe('Build');
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
expect(getStageColumnTitle().text()).toBe('');
});
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(layersFn).toHaveBeenCalledTimes(1);
await getViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
await getViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
expect(layersFn).toHaveBeenCalledTimes(1);
});
}); });
}); });
}); });
...@@ -2,10 +2,17 @@ import { mount, shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -2,10 +2,17 @@ import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { DOWNSTREAM, GRAPHQL, UPSTREAM } from '~/pipelines/components/graph/constants'; import {
DOWNSTREAM,
GRAPHQL,
UPSTREAM,
LAYER_VIEW,
STAGE_VIEW,
} from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue'; import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue'; import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import { LOAD_FAILURE } from '~/pipelines/constants'; import { LOAD_FAILURE } from '~/pipelines/constants';
import { import {
mockPipelineResponse, mockPipelineResponse,
...@@ -20,6 +27,7 @@ describe('Linked Pipelines Column', () => { ...@@ -20,6 +27,7 @@ describe('Linked Pipelines Column', () => {
columnTitle: 'Downstream', columnTitle: 'Downstream',
linkedPipelines: processedPipeline.downstream, linkedPipelines: processedPipeline.downstream,
type: DOWNSTREAM, type: DOWNSTREAM,
viewType: STAGE_VIEW,
configPaths: { configPaths: {
metricsPath: '', metricsPath: '',
graphqlResourceEtag: 'this/is/a/path', graphqlResourceEtag: 'this/is/a/path',
...@@ -67,7 +75,7 @@ describe('Linked Pipelines Column', () => { ...@@ -67,7 +75,7 @@ describe('Linked Pipelines Column', () => {
describe('it renders correctly', () => { describe('it renders correctly', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponentWithApollo();
}); });
it('renders the pipeline title', () => { it('renders the pipeline title', () => {
...@@ -91,6 +99,27 @@ describe('Linked Pipelines Column', () => { ...@@ -91,6 +99,27 @@ describe('Linked Pipelines Column', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}; };
describe('layer type rendering', () => {
let layersFn;
beforeEach(() => {
layersFn = jest.spyOn(parsingUtils, 'listByLayers');
createComponentWithApollo({ mountFn: mount });
});
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
await clickExpandButtonAndAwaitTimers();
await wrapper.setProps({ viewType: LAYER_VIEW });
await wrapper.vm.$nextTick();
expect(layersFn).toHaveBeenCalledTimes(1);
await wrapper.setProps({ viewType: STAGE_VIEW });
await wrapper.setProps({ viewType: LAYER_VIEW });
await wrapper.setProps({ viewType: STAGE_VIEW });
expect(layersFn).toHaveBeenCalledTimes(1);
});
});
describe('downstream', () => { describe('downstream', () => {
describe('when successful', () => { describe('when successful', () => {
beforeEach(() => { beforeEach(() => {
......
...@@ -434,21 +434,7 @@ export const mockPipelineResponse = { ...@@ -434,21 +434,7 @@ export const mockPipelineResponse = {
}, },
needs: { needs: {
__typename: 'CiBuildNeedConnection', __typename: 'CiBuildNeedConnection',
nodes: [ nodes: [],
{
__typename: 'CiBuildNeed',
name: 'build_c',
},
{
__typename: 'CiBuildNeed',
name: 'build_b',
},
{
__typename: 'CiBuildNeed',
name:
'build_a_nlfjkdnlvskfnksvjknlfdjvlvnjdkjdf_nvjkenjkrlngjeknjkl',
},
],
}, },
}, },
], ],
......
...@@ -96,11 +96,11 @@ const completeMock = [ ...@@ -96,11 +96,11 @@ const completeMock = [
describe('Shared pipeline unwrapping utils', () => { describe('Shared pipeline unwrapping utils', () => {
describe('unwrapGroups', () => { describe('unwrapGroups', () => {
it('takes stages without nodes and returns the unwrapped groups', () => { it('takes stages without nodes and returns the unwrapped groups', () => {
expect(unwrapGroups(stagesAndGroups)[0].groups).toEqual(groupsArray); expect(unwrapGroups(stagesAndGroups)[0].node.groups).toEqual(groupsArray);
}); });
it('keeps other stage properties intact', () => { it('keeps other stage properties intact', () => {
expect(unwrapGroups(stagesAndGroups)[0]).toMatchObject(basicStageInfo); expect(unwrapGroups(stagesAndGroups)[0].node).toMatchObject(basicStageInfo);
}); });
}); });
......
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