Welcome to mirror list, hosted at ThFree Co, Russian Federation.

parsing_utils.js « components « pipelines « javascripts « assets « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: cae4e11c13fb3acda82273080a4e4acc1b9e41ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import { memoize } from 'lodash';
import { createNodeDict } from '../utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
import { createSankey } from './dag/drawing_utils';

/*
  A peformant alternative to lodash's isEqual. Because findIndex always finds
  the first instance of a match, if the found index is not the first, we know
  it is in fact a duplicate.
*/
const deduplicate = (item, itemIndex, arr) => {
  const foundIdx = arr.findIndex((test) => {
    return test.source === item.source && test.target === item.target;
  });

  return foundIdx === itemIndex;
};

export const makeLinksFromNodes = (nodes, nodeDict, { needsKey = NEEDS_PROPERTY } = {}) => {
  const constantLinkValue = 10; // all links are the same weight
  return nodes
    .map(({ jobs, name: groupName }) =>
      jobs.map((job) => {
        const needs = job[needsKey] || [];

        return needs.reduce((acc, needed) => {
          // It's possible that we have an optional job, which
          // is being needed by another job. In that scenario,
          // the needed job doesn't exist, so we don't want to
          // create link for it.
          if (nodeDict[needed]?.name) {
            acc.push({
              source: nodeDict[needed].name,
              target: groupName,
              value: constantLinkValue,
            });
          }

          return acc;
        }, []);
      }),
    )
    .flat(2);
};

export const getAllAncestors = (nodes, nodeDict) => {
  const needs = nodes
    .map((node) => {
      return nodeDict[node]?.needs || '';
    })
    .flat()
    .filter(Boolean)
    .filter(deduplicate);

  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;
    const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source);
    const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
    return !allAncestors.includes(source);
  });

export const parseData = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => {
  const nodeDict = createNodeDict(nodes, { needsKey });
  const allLinks = makeLinksFromNodes(nodes, nodeDict, { needsKey });
  const filteredLinks = allLinks.filter(deduplicate);
  const links = filterByAncestors(filteredLinks, nodeDict);

  return { nodes, links };
};

/*
  The number of nodes in the most populous generation drives the height of the graph.
*/

export const getMaxNodes = (nodes) => {
  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.
*/

export const removeOrphanNodes = (sankeyfiedNodes) => {
  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 explicitParsedData = parseData(arrayOfJobs, { needsKey: EXPLICIT_NEEDS_PROPERTY });
  const dataWithLayers = createSankey()(explicitParsedData);

  const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
    /* sort groups by layer */

    if (!acc[layer]) {
      acc[layer] = [];
    }

    acc[layer].push(name);

    return acc;
  }, []);

  return {
    linksData: parsedData.links,
    numGroups: arrayOfJobs.length,
    pipelineLayers,
  };
};

export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
  return 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 } = stagesLookup[id];
      return stages[stageIdx]?.groups?.[groupIdx];
    });

    return {
      name: '',
      id: `layer-${idx}`,
      status: { action: null },
      groups: groups.filter(Boolean),
    };
  });
};

export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFromLayersListBare);