debian-mirror-gitlab/app/assets/javascripts/pipelines/components/parsing_utils.js

173 lines
4.8 KiB
JavaScript
Raw Normal View History

2020-06-23 00:09:42 +05:30
import { uniqWith, isEqual } from 'lodash';
2021-04-29 21:17:54 +05:30
import { createSankey } from './dag/drawing_utils';
2020-06-23 00:09:42 +05:30
/*
The following functions are the main engine in transforming the data as
received from the endpoint into the format the d3 graph expects.
Input is of the form:
2020-10-24 23:57:45 +05:30
[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
2020-06-23 00:09:42 +05:30
Output is of the form:
{ nodes: [node], links: [link] }
node: { name, category }, + unused info passed through
link: { source, target, value }, with source & target being node names
and value being a constant
2020-10-24 23:57:45 +05:30
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
2020-06-23 00:09:42 +05:30
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 LINKS
2020-10-24 23:57:45 +05:30
nodes.name -> target
nodes.name.needs.each -> source (source is the name of the group, not the parallel job)
2020-06-23 00:09:42 +05:30
10 -> value (constant)
*/
2021-03-08 18:12:59 +05:30
export const createNodeDict = (nodes) => {
2020-06-23 00:09:42 +05:30
return nodes.reduce((acc, node) => {
const newNode = {
...node,
2021-03-08 18:12:59 +05:30
needs: node.jobs.map((job) => job.needs || []).flat(),
2020-06-23 00:09:42 +05:30
};
if (node.size > 1) {
2021-03-08 18:12:59 +05:30
node.jobs.forEach((job) => {
2020-06-23 00:09:42 +05:30
acc[job.name] = newNode;
});
}
acc[node.name] = newNode;
return acc;
}, {});
};
export const makeLinksFromNodes = (nodes, nodeDict) => {
const constantLinkValue = 10; // all links are the same weight
return nodes
2021-03-08 18:12:59 +05:30
.map((group) => {
return group.jobs.map((job) => {
2020-06-23 00:09:42 +05:30
if (!job.needs) {
return [];
}
2021-03-08 18:12:59 +05:30
return job.needs.map((needed) => {
2020-06-23 00:09:42 +05:30
return {
source: nodeDict[needed]?.name,
target: group.name,
value: constantLinkValue,
};
});
});
})
.flat(2);
};
export const getAllAncestors = (nodes, nodeDict) => {
const needs = nodes
2021-03-08 18:12:59 +05:30
.map((node) => {
2020-06-23 00:09:42 +05:30
return nodeDict[node].needs || '';
})
.flat()
.filter(Boolean);
if (needs.length) {
return [...needs, ...getAllAncestors(needs, nodeDict)];
}
return [];
};
export const filterByAncestors = (links, nodeDict) =>
links.filter(({ target, source }) => {
/*
for every link, check out it's target
for every target, get the target node's needs
then drop the current link source from that list
call a function to get all ancestors, recursively
is the current link's source in the list of all parents?
then we drop this link
*/
const targetNode = target;
const targetNodeNeeds = nodeDict[targetNode].needs;
2021-03-08 18:12:59 +05:30
const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source);
2020-06-23 00:09:42 +05:30
const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
return !allAncestors.includes(source);
});
2021-03-08 18:12:59 +05:30
export const parseData = (nodes) => {
2020-10-24 23:57:45 +05:30
const nodeDict = createNodeDict(nodes);
2020-06-23 00:09:42 +05:30
const allLinks = makeLinksFromNodes(nodes, nodeDict);
const filteredLinks = filterByAncestors(allLinks, nodeDict);
const links = uniqWith(filteredLinks, isEqual);
return { nodes, links };
};
/*
The number of nodes in the most populous generation drives the height of the graph.
*/
2021-03-08 18:12:59 +05:30
export const getMaxNodes = (nodes) => {
2020-06-23 00:09:42 +05:30
const counts = nodes.reduce((acc, { layer }) => {
if (!acc[layer]) {
acc[layer] = 0;
}
acc[layer] += 1;
return acc;
}, []);
return Math.max(...counts);
};
/*
Because we cannot know if a node is part of a relationship until after we
generate the links with createSankey, this function is used after the first call
to find nodes that have no relations.
*/
2021-03-08 18:12:59 +05:30
export const removeOrphanNodes = (sankeyfiedNodes) => {
return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length);
2020-06-23 00:09:42 +05:30
};
2021-04-29 21:17:54 +05:30
/*
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;
}, []);
};