Commit 2ac47a71 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'dag-graphql-fe' into 'master'

Convert DAG to Use GraphQL

See merge request gitlab-org/gitlab!38085
parents 77614c16 182f2616
<script>
import { GlAlert, GlButton, GlEmptyState, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
import DagGraph from './dag_graph.vue';
import DagAnnotations from './dag_annotations.vue';
import {
......@@ -27,23 +28,58 @@ export default {
GlEmptyState,
GlButton,
},
props: {
graphUrl: {
type: String,
required: false,
default: '',
inject: {
dagDocPath: {
default: null,
},
emptySvgPath: {
type: String,
required: true,
default: '',
},
dagDocPath: {
type: String,
required: true,
pipelineIid: {
default: '',
},
pipelineProjectPath: {
default: '',
},
},
apollo: {
graphData: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getDagVisData,
variables() {
return {
projectPath: this.pipelineProjectPath,
iid: this.pipelineIid,
};
},
update(data) {
const {
stages: { nodes: stages },
} = data.project.pipeline;
const unwrappedGroups = stages
.map(({ name, groups: { nodes: groups } }) => {
return groups.map(group => {
return { category: name, ...group };
});
})
.flat(2);
const nodes = unwrappedGroups.map(group => {
const jobs = group.jobs.nodes.map(({ name, needs }) => {
return { name, needs: needs.nodes.map(need => need.name) };
});
return { ...group, jobs };
});
return nodes;
},
error() {
this.reportFailure(LOAD_FAILURE);
},
},
},
data() {
return {
annotationsMap: {},
......@@ -90,32 +126,20 @@ export default {
default:
return {
text: this.$options.errorTexts[DEFAULT],
vatiant: 'danger',
variant: 'danger',
};
}
},
processedData() {
return this.processGraphData(this.graphData);
},
shouldDisplayAnnotations() {
return !isEmpty(this.annotationsMap);
},
shouldDisplayGraph() {
return Boolean(!this.showFailureAlert && this.graphData);
return Boolean(!this.showFailureAlert && !this.hasNoDependentJobs && this.graphData);
},
},
mounted() {
const { processGraphData, reportFailure } = this;
if (!this.graphUrl) {
reportFailure();
return;
}
axios
.get(this.graphUrl)
.then(response => {
processGraphData(response.data);
})
.catch(() => reportFailure(LOAD_FAILURE));
},
methods: {
addAnnotationToMap({ uid, source, target }) {
this.$set(this.annotationsMap, uid, { source, target });
......@@ -124,25 +148,25 @@ export default {
let parsed;
try {
parsed = parseData(data.stages);
parsed = parseData(data);
} catch {
this.reportFailure(PARSE_FAILURE);
return;
return {};
}
if (parsed.links.length === 1) {
this.reportFailure(UNSUPPORTED_DATA);
return;
return {};
}
// If there are no links, we don't report failure
// as it simply means the user does not use job dependencies
if (parsed.links.length === 0) {
this.hasNoDependentJobs = true;
return;
return {};
}
this.graphData = parsed;
return parsed;
},
hideAlert() {
this.showFailureAlert = false;
......@@ -182,7 +206,7 @@ export default {
<dag-annotations v-if="shouldDisplayAnnotations" :annotations="annotationsMap" />
<dag-graph
v-if="shouldDisplayGraph"
:graph-data="graphData"
:graph-data="processedData"
@onFailure="reportFailure"
@update-annotation="updateAnnotation"
/>
......@@ -209,7 +233,7 @@ export default {
</p>
</div>
</template>
<template #actions>
<template v-if="dagDocPath" #actions>
<gl-button :href="dagDocPath" target="__blank" variant="success">
{{ $options.emptyStateTexts.button }}
</gl-button>
......
......@@ -5,14 +5,16 @@ import { uniqWith, isEqual } from 'lodash';
received from the endpoint into the format the d3 graph expects.
Input is of the form:
[stages]
stages: {name, groups}
groups: [{ name, size, jobs }]
name is a group name; in the case that the group has one job, it is
also the job name
size is the number of parallel jobs
jobs: [{ name, needs}]
job name is either the same as the group name or group x/y
[nodes]
nodes: [{category, name, jobs, size}]
category is the stage name
name is a group name; in the case that the group has one job, it is
also the job name
size is the number of parallel jobs
jobs: [{ name, needs}]
job name is either the same as the group name or group x/y
needs: [job-names]
needs is an array of job-name strings
Output is of the form:
{ nodes: [node], links: [link] }
......@@ -20,30 +22,17 @@ import { uniqWith, isEqual } from 'lodash';
link: { source, target, value }, with source & target being node names
and value being a constant
We create nodes, create links, and then dedupe the links, so that in the case where
We create nodes in the GraphQL update function, and then here we create the node dictionary,
then create links, and then dedupe the links, so that in the case where
job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
from job 1 to job 2 then another from job 2 to job 4.
CREATE NODES
stage.name -> node.category
stage.group.name -> node.name (this is the group name if there are parallel jobs)
stage.group.jobs -> node.jobs
stage.group.size -> node.size
CREATE LINKS
stages.groups.name -> target
stages.groups.needs.each -> source (source is the name of the group, not the parallel job)
nodes.name -> target
nodes.name.needs.each -> source (source is the name of the group, not the parallel job)
10 -> value (constant)
*/
export const createNodes = data => {
return data.flatMap(({ groups, name }) => {
return groups.map(group => {
return { ...group, category: name };
});
});
};
export const createNodeDict = nodes => {
return nodes.reduce((acc, node) => {
const newNode = {
......@@ -62,13 +51,6 @@ export const createNodeDict = nodes => {
}, {});
};
export const createNodesStructure = data => {
const nodes = createNodes(data);
const nodeDict = createNodeDict(nodes);
return { nodes, nodeDict };
};
export const makeLinksFromNodes = (nodes, nodeDict) => {
const constantLinkValue = 10; // all links are the same weight
return nodes
......@@ -126,8 +108,8 @@ export const filterByAncestors = (links, nodeDict) =>
return !allAncestors.includes(source);
});
export const parseData = data => {
const { nodes, nodeDict } = createNodesStructure(data);
export const parseData = nodes => {
const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict);
const filteredLinks = filterByAncestors(allLinks, nodeDict);
const links = uniqWith(filteredLinks, isEqual);
......
query getDagVisData($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
stages {
nodes {
name
groups {
nodes {
name
size
jobs {
nodes {
name
needs {
nodes {
name
}
}
}
}
}
}
}
}
}
}
}
......@@ -4,7 +4,7 @@ import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import pipelineGraph from './components/graph/graph_component.vue';
import Dag from './components/dag/dag.vue';
import createDagApp from './pipeline_details_dag';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue';
......@@ -114,32 +114,6 @@ const createTestDetails = () => {
});
};
const createDagApp = () => {
if (!window.gon?.features?.dagPipelineTab) {
return;
}
const el = document.querySelector('#js-pipeline-dag-vue');
const { pipelineDataPath, emptySvgPath, dagDocPath } = el?.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
Dag,
},
render(createElement) {
return createElement('dag', {
props: {
graphUrl: pipelineDataPath,
emptySvgPath,
dagDocPath,
},
});
},
});
};
export default () => {
const { dataset } = document.querySelector('.js-pipeline-details-vue');
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import Dag from './components/dag/dag.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const createDagApp = () => {
if (!window.gon?.features?.dagPipelineTab) {
return;
}
const el = document.querySelector('#js-pipeline-dag-vue');
const { pipelineProjectPath, pipelineIid, emptySvgPath, dagDocPath } = el?.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
Dag,
},
apolloProvider,
provide: {
pipelineProjectPath,
pipelineIid,
emptySvgPath,
dagDocPath,
},
render(createElement) {
return createElement('dag', {});
},
});
};
export default createDagApp;
......@@ -81,7 +81,7 @@
- if dag_pipeline_tab_enabled
#js-tab-dag.tab-pane
#js-pipeline-dag-vue{ data: { pipeline_data_path: dag_project_pipeline_path(@project, @pipeline), empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
#js-pipeline-dag-vue{ data: { pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
#js-tab-tests.tab-pane
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
......
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Dag from '~/pipelines/components/dag/dag.vue';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
......@@ -11,13 +8,11 @@ import {
ADD_NOTE,
REMOVE_NOTE,
REPLACE_NOTES,
DEFAULT,
PARSE_FAILURE,
LOAD_FAILURE,
UNSUPPORTED_DATA,
} from '~/pipelines/components/dag//constants';
import {
mockBaseData,
mockParsedGraphQLNodes,
tooSmallGraph,
unparseableGraph,
graphWithoutDependencies,
......@@ -27,7 +22,6 @@ import {
describe('Pipeline DAG graph wrapper', () => {
let wrapper;
let mock;
const getAlert = () => wrapper.find(GlAlert);
const getAllAlerts = () => wrapper.findAll(GlAlert);
const getGraph = () => wrapper.find(DagGraph);
......@@ -35,45 +29,46 @@ describe('Pipeline DAG graph wrapper', () => {
const getErrorText = type => wrapper.vm.$options.errorTexts[type];
const getEmptyState = () => wrapper.find(GlEmptyState);
const dataPath = '/root/test/pipelines/90/dag.json';
const createComponent = (propsData = {}, method = shallowMount) => {
const createComponent = ({
graphData = mockParsedGraphQLNodes,
provideOverride = {},
method = shallowMount,
} = {}) => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = method(Dag, {
propsData: {
provide: {
pipelineProjectPath: 'root/abc-dag',
pipelineIid: '1',
emptySvgPath: '/my-svg',
dagDocPath: '/my-doc',
...propsData,
...provideOverride,
},
data() {
return {
graphData,
showFailureAlert: false,
};
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
wrapper.destroy();
wrapper = null;
});
describe('when there is no dataUrl', () => {
describe('when a query argument is undefined', () => {
beforeEach(() => {
createComponent({ graphUrl: undefined });
createComponent({
provideOverride: { pipelineProjectPath: undefined },
graphData: null,
});
});
it('shows the DEFAULT alert and not the graph', () => {
expect(getAlert().exists()).toBe(true);
expect(getAlert().text()).toBe(getErrorText(DEFAULT));
it('does not render the graph', async () => {
expect(getGraph().exists()).toBe(false);
});
......@@ -82,36 +77,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe('when there is a dataUrl', () => {
describe('but the data fetch fails', () => {
describe('when all query variables are defined', () => {
describe('but the parse fails', () => {
beforeEach(async () => {
mock.onGet(dataPath).replyOnce(500);
createComponent({ graphUrl: dataPath });
await wrapper.vm.$nextTick();
return waitForPromises();
});
it('shows the LOAD_FAILURE alert and not the graph', () => {
expect(getAlert().exists()).toBe(true);
expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE));
expect(getGraph().exists()).toBe(false);
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('the data fetch succeeds but the parse fails', () => {
beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, unparseableGraph);
createComponent({ graphUrl: dataPath });
await wrapper.vm.$nextTick();
return waitForPromises();
createComponent({
graphData: unparseableGraph,
});
});
it('shows the PARSE_FAILURE alert and not the graph', () => {
......@@ -125,14 +96,9 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe('and the data fetch and parse succeeds', () => {
describe('parse succeeds', () => {
beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, mockBaseData);
createComponent({ graphUrl: dataPath }, mount);
await wrapper.vm.$nextTick();
return waitForPromises();
createComponent({ method: mount });
});
it('shows the graph', () => {
......@@ -144,14 +110,11 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe('the data fetch and parse succeeds, but the resulting graph is too small', () => {
describe('parse succeeds, but the resulting graph is too small', () => {
beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, tooSmallGraph);
createComponent({ graphUrl: dataPath });
await wrapper.vm.$nextTick();
return waitForPromises();
createComponent({
graphData: tooSmallGraph,
});
});
it('shows the UNSUPPORTED_DATA alert and not the graph', () => {
......@@ -165,14 +128,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
describe('the data fetch succeeds but the returned data is empty', () => {
describe('the returned data is empty', () => {
beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, graphWithoutDependencies);
createComponent({ graphUrl: dataPath }, mount);
await wrapper.vm.$nextTick();
return waitForPromises();
createComponent({
method: mount,
graphData: graphWithoutDependencies,
});
});
it('does not render an error alert or the graph', () => {
......@@ -188,12 +149,7 @@ describe('Pipeline DAG graph wrapper', () => {
describe('annotations', () => {
beforeEach(async () => {
mock.onGet(dataPath).replyOnce(200, mockBaseData);
createComponent({ graphUrl: dataPath }, mount);
await wrapper.vm.$nextTick();
return waitForPromises();
createComponent();
});
it('toggles on link mouseover and mouseout', async () => {
......
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { parseData } from '~/pipelines/components/dag/parsing_utils';
import { mockBaseData } from './mock_data';
import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization drawing utilities', () => {
const parsed = parseData(mockBaseData.stages);
const parsed = parseData(mockParsedGraphQLNodes);
const layoutSettings = {
width: 200,
......
/*
It is important that the simple base include parallel jobs
as well as non-parallel jobs with spaces in the name to prevent
us relying on spaces as an indicator.
*/
export const mockBaseData = {
stages: [
{
name: 'test',
groups: [
{
name: 'jest',
size: 2,
jobs: [{ name: 'jest 1/2', needs: ['frontend fixtures'] }, { name: 'jest 2/2' }],
},
{
name: 'rspec',
size: 1,
jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
},
],
},
{
name: 'fixtures',
groups: [
{
name: 'frontend fixtures',
size: 1,
jobs: [{ name: 'frontend fixtures' }],
},
],
},
{
name: 'un-needed',
groups: [
{
name: 'un-needed',
size: 1,
jobs: [{ name: 'un-needed' }],
},
],
},
],
};
export const tooSmallGraph = {
stages: [
{
name: 'test',
groups: [
{
name: 'jest',
size: 2,
jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
},
{
name: 'rspec',
size: 1,
jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
},
],
},
{
name: 'fixtures',
groups: [
{
name: 'frontend fixtures',
size: 1,
jobs: [{ name: 'frontend fixtures' }],
},
],
},
{
name: 'un-needed',
groups: [
{
name: 'un-needed',
size: 1,
jobs: [{ name: 'un-needed' }],
},
],
},
],
};
export const tooSmallGraph = [
{
category: 'test',
name: 'jest',
size: 2,
jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
},
{
category: 'test',
name: 'rspec',
size: 1,
jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
},
{
category: 'fixtures',
name: 'frontend fixtures',
size: 1,
jobs: [{ name: 'frontend fixtures' }],
},
{
category: 'un-needed',
name: 'un-needed',
size: 1,
jobs: [{ name: 'un-needed' }],
},
];
export const graphWithoutDependencies = {
stages: [
{
name: 'test',
groups: [
{
name: 'jest',
size: 2,
jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
},
{
name: 'rspec',
size: 1,
jobs: [{ name: 'rspec' }],
},
],
},
{
name: 'fixtures',
groups: [
{
name: 'frontend fixtures',
size: 1,
jobs: [{ name: 'frontend fixtures' }],
},
],
},
{
name: 'un-needed',
groups: [
{
name: 'un-needed',
size: 1,
jobs: [{ name: 'un-needed' }],
},
],
},
],
};
export const graphWithoutDependencies = [
{
category: 'test',
name: 'jest',
size: 2,
jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
},
{
category: 'test',
name: 'rspec',
size: 1,
jobs: [{ name: 'rspec' }],
},
{
category: 'fixtures',
name: 'frontend fixtures',
size: 1,
jobs: [{ name: 'frontend fixtures' }],
},
{
category: 'un-needed',
name: 'un-needed',
size: 1,
jobs: [{ name: 'un-needed' }],
},
];
export const unparseableGraph = [
{
......@@ -468,3 +397,264 @@ export const multiNote = {
},
},
};
/*
It is important that the base include parallel jobs
as well as non-parallel jobs with spaces in the name to prevent
us relying on spaces as an indicator.
*/
export const mockParsedGraphQLNodes = [
{
category: 'build',
name: 'build_a',
size: 1,
jobs: [
{
name: 'build_a',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'build',
name: 'build_b',
size: 1,
jobs: [
{
name: 'build_b',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'test',
name: 'test_a',
size: 1,
jobs: [
{
name: 'test_a',
needs: ['build_a'],
},
],
__typename: 'CiGroup',
},
{
category: 'test',
name: 'test_b',
size: 1,
jobs: [
{
name: 'test_b',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'test',
name: 'test_c',
size: 1,
jobs: [
{
name: 'test_c',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'test',
name: 'test_d',
size: 1,
jobs: [
{
name: 'test_d',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'post-test',
name: 'post_test_a',
size: 1,
jobs: [
{
name: 'post_test_a',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'post-test',
name: 'post_test_b',
size: 1,
jobs: [
{
name: 'post_test_b',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'post-test',
name: 'post_test_c',
size: 1,
jobs: [
{
name: 'post_test_c',
needs: ['test_b', 'test_a'],
},
],
__typename: 'CiGroup',
},
{
category: 'staging',
name: 'staging_a',
size: 1,
jobs: [
{
name: 'staging_a',
needs: ['post_test_a'],
},
],
__typename: 'CiGroup',
},
{
category: 'staging',
name: 'staging_b',
size: 1,
jobs: [
{
name: 'staging_b',
needs: ['post_test_b'],
},
],
__typename: 'CiGroup',
},
{
category: 'staging',
name: 'staging_c',
size: 1,
jobs: [
{
name: 'staging_c',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'staging',
name: 'staging_d',
size: 1,
jobs: [
{
name: 'staging_d',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'staging',
name: 'staging_e',
size: 1,
jobs: [
{
name: 'staging_e',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'canary',
name: 'canary_a',
size: 1,
jobs: [
{
name: 'canary_a',
needs: ['staging_b', 'staging_a'],
},
],
__typename: 'CiGroup',
},
{
category: 'canary',
name: 'canary_b',
size: 1,
jobs: [
{
name: 'canary_b',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'canary',
name: 'canary_c',
size: 1,
jobs: [
{
name: 'canary_c',
needs: ['staging_b'],
},
],
__typename: 'CiGroup',
},
{
category: 'production',
name: 'production_a',
size: 1,
jobs: [
{
name: 'production_a',
needs: ['canary_a'],
},
],
__typename: 'CiGroup',
},
{
category: 'production',
name: 'production_b',
size: 1,
jobs: [
{
name: 'production_b',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'production',
name: 'production_c',
size: 1,
jobs: [
{
name: 'production_c',
needs: [],
},
],
__typename: 'CiGroup',
},
{
category: 'production',
name: 'production_d',
size: 1,
jobs: [
{
name: 'production_d',
needs: ['canary_c'],
},
],
__typename: 'CiGroup',
},
];
import {
createNodesStructure,
createNodeDict,
makeLinksFromNodes,
filterByAncestors,
parseData,
......@@ -8,56 +8,17 @@ import {
} from '~/pipelines/components/dag/parsing_utils';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { mockBaseData } from './mock_data';
import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization parsing utilities', () => {
const { nodes, nodeDict } = createNodesStructure(mockBaseData.stages);
const unfilteredLinks = makeLinksFromNodes(nodes, nodeDict);
const parsed = parseData(mockBaseData.stages);
const layoutSettings = {
width: 200,
height: 200,
nodeWidth: 10,
nodePadding: 20,
paddingForLabels: 100,
};
const sankeyLayout = createSankey(layoutSettings)(parsed);
describe('createNodesStructure', () => {
const parallelGroupName = 'jest';
const parallelJobName = 'jest 1/2';
const singleJobName = 'frontend fixtures';
const { name, jobs, size } = mockBaseData.stages[0].groups[0];
it('returns the expected node structure', () => {
expect(nodes[0]).toHaveProperty('category', mockBaseData.stages[0].name);
expect(nodes[0]).toHaveProperty('name', name);
expect(nodes[0]).toHaveProperty('jobs', jobs);
expect(nodes[0]).toHaveProperty('size', size);
});
it('adds needs to top level of nodeDict entries', () => {
expect(nodeDict[parallelGroupName]).toHaveProperty('needs');
expect(nodeDict[parallelJobName]).toHaveProperty('needs');
expect(nodeDict[singleJobName]).toHaveProperty('needs');
});
it('makes entries in nodeDict for jobs and parallel jobs', () => {
const nodeNames = Object.keys(nodeDict);
expect(nodeNames.includes(parallelGroupName)).toBe(true);
expect(nodeNames.includes(parallelJobName)).toBe(true);
expect(nodeNames.includes(singleJobName)).toBe(true);
});
});
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
const unfilteredLinks = makeLinksFromNodes(mockParsedGraphQLNodes, nodeDict);
const parsed = parseData(mockParsedGraphQLNodes);
describe('makeLinksFromNodes', () => {
it('returns the expected link structure', () => {
expect(unfilteredLinks[0]).toHaveProperty('source', 'frontend fixtures');
expect(unfilteredLinks[0]).toHaveProperty('target', 'jest');
expect(unfilteredLinks[0]).toHaveProperty('source', 'build_a');
expect(unfilteredLinks[0]).toHaveProperty('target', 'test_a');
expect(unfilteredLinks[0]).toHaveProperty('value', 10);
});
});
......@@ -107,8 +68,22 @@ describe('DAG visualization parsing utilities', () => {
describe('removeOrphanNodes', () => {
it('removes sankey nodes that have no needs and are not needed', () => {
const layoutSettings = {
width: 200,
height: 200,
nodeWidth: 10,
nodePadding: 20,
paddingForLabels: 100,
};
const sankeyLayout = createSankey(layoutSettings)(parsed);
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
expect(cleanedNodes).toHaveLength(sankeyLayout.nodes.length - 1);
/*
These lengths are determined by the mock data.
If the data changes, the numbers may also change.
*/
expect(parsed.nodes).toHaveLength(21);
expect(cleanedNodes).toHaveLength(12);
});
});
......
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