@@ -46,12 +46,21 @@ import {
4646 */
4747function addWorkflow ( workflow , gscan , options ) {
4848 const hierarchical = options . hierarchical || true
49+ const workflowNode = createWorkflowNode ( workflow , hierarchical )
4950 if ( hierarchical ) {
50- const workflowNode = createWorkflowNode ( workflow , hierarchical )
51- addHierarchicalWorkflow ( workflowNode , gscan . lookup , gscan . tree , options )
51+ // TBD: We need the leaf node to propagate states, and this is done here since the
52+ // addHierarchicalWorkflow has recursion. There might be a better way for
53+ // handling this though?
54+ let leafNode = workflowNode
55+ while ( leafNode . children ) {
56+ // [0] because this is not really a sparse tree, but more like a linked-list since
57+ // we created the node with createWorkflowNode.
58+ leafNode = leafNode . children [ 0 ]
59+ }
60+ addHierarchicalWorkflow ( workflowNode , leafNode , gscan . lookup , gscan . tree , options )
5261 } else {
53- gscan . lookup [ workflow . id ] = workflow
54- gscan . tree . push ( workflow )
62+ gscan . lookup [ workflow . id ] = workflowNode
63+ gscan . tree . push ( workflowNode )
5564 }
5665}
5766
@@ -60,18 +69,19 @@ function addWorkflow (workflow, gscan, options) {
6069 * functions of this module). This is required as we apply recursion for adding nodes into the tree,
6170 * but we replace the tree and pass only a sub-tree.
6271 *
72+ * @param workflowOrPart
6373 * @param workflow
6474 * @param {Lookup } lookup
6575 * @param {Array<TreeNode> } tree
6676 * @param {* } options
6777 * @private
6878 */
69- function addHierarchicalWorkflow ( workflow , lookup , tree , options ) {
70- if ( ! lookup [ workflow . id ] ) {
71- // a new node, let 's add this node and its descendants to the lookup
72- lookup [ workflow . id ] = workflow
73- if ( workflow . children ) {
74- const stack = [ ...workflow . children ]
79+ function addHierarchicalWorkflow ( workflowOrPart , workflow , lookup , tree , options ) {
80+ if ( ! lookup [ workflowOrPart . id ] ) {
81+ // A new node. Let 's add this node and its descendants to the lookup.
82+ lookup [ workflowOrPart . id ] = workflowOrPart
83+ if ( workflowOrPart . children ) {
84+ const stack = [ ...workflowOrPart . children ]
7585 while ( stack . length ) {
7686 const currentNode = stack . shift ( )
7787 lookup [ currentNode . id ] = currentNode
@@ -80,32 +90,38 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
8090 }
8191 }
8292 }
83- // and now add the top-level node to the tree
84- // Here we calculate what is the index for this element. If we decide to have ASC and DESC,
85- // then we just need to invert the location of the element, something like
86- // `sortedIndex = (array.length - sortedIndex)`.
93+ // And now add the node to the tree. Here we calculate what is the index for this element.
94+ // If we decide to have ASC and DESC, then we just need to invert the location of the node,
95+ // something like `sortedIndex = (array.length - sortedIndex)`.
8796 const sortedIndex = sortedIndexBy (
8897 tree ,
89- workflow ,
98+ workflowOrPart ,
9099 ( n ) => n . name ,
91100 sortWorkflowNamePartNodeOrWorkflowNode
92101 )
93- tree . splice ( sortedIndex , 0 , workflow )
102+ tree . splice ( sortedIndex , 0 , workflowOrPart )
94103 } else {
95- // we will have to merge the hierarchies
96- const existingNode = lookup [ workflow . id ]
97- // TODO: combine states summaries?
104+ // The node exists in the lookup, so must exist in the tree too. We will have to merge the hierarchies.
105+ const existingNode = lookup [ workflowOrPart . id ]
98106 if ( existingNode . children ) {
107+ // Propagate workflow states to its ancestor.
108+ if ( workflow . node . latestStateTasks && workflow . node . stateTotals ) {
109+ existingNode . node . descendantsLatestStateTasks [ workflow . id ] = workflow . node . latestStateTasks
110+ existingNode . node . descendantsStateTotal [ workflow . id ] = workflow . node . stateTotals
111+ tallyPropagatedStates ( existingNode . node )
112+ }
99113 // Copy array since we will iterate it, and modify existingNode.children
100114 // (see the tree.splice above.)
101- const children = [ ...workflow . children ]
115+ const children = [ ...workflowOrPart . children ]
102116 for ( const child of children ) {
103- // Recursion
104- addHierarchicalWorkflow ( child , lookup , existingNode . children , options )
117+ // Recursion!
118+ addHierarchicalWorkflow ( child , workflow , lookup , existingNode . children , options )
105119 }
106120 } else {
107- // Here we have an existing workflow node. Let's merge it.
108- mergeWith ( existingNode , workflow , mergeWithCustomizer )
121+ // Here we have an existing workflow node (only child-less). Let's merge it.
122+ // It should not happen actually, since this is adding a workflow. Maybe throw
123+ // an error instead?
124+ mergeWith ( existingNode , workflowOrPart , mergeWithCustomizer )
109125 }
110126 }
111127}
@@ -129,24 +145,21 @@ function updateWorkflow (workflow, gscan, options) {
129145 mergeWith ( existingData . node , workflow , mergeWithCustomizer )
130146 const hierarchical = options . hierarchical || true
131147 if ( hierarchical ) {
148+ // But now we need to propagate the states up to its ancestors, if any.
132149 updateHierarchicalWorkflow ( existingData , gscan . lookup , gscan . tree , options )
133150 }
134- // TODO: create workflow hierarchy (from workflow object), then iterate
135- // it and use lookup to fetch the existing node. Finally, combine
136- // the gscan states (latestStateTasks & stateTotals).
137151 Vue . set ( gscan . lookup , existingData . id , existingData )
138152}
139153
140154function updateHierarchicalWorkflow ( existingData , lookup , tree , options ) {
141- // We need to sort its parent again.
142155 const workflowNameParts = parseWorkflowNameParts ( existingData . id )
156+ // nodesIds contains the list of GScan tree nodes, with the workflow being a leaf node.
143157 const nodesIds = getWorkflowNamePartsNodesIds ( workflowNameParts )
144- // Discard the last since it's the workflow ID that we already have
145- // in the `existingData` object. Now if not empty, we have our parent.
146- nodesIds . pop ( )
158+ const workflowId = nodesIds . pop ( )
147159 const parentId = nodesIds . length > 0 ? nodesIds . pop ( ) : null
148160 const parent = parentId ? lookup [ parentId ] : tree
149161 if ( ! parent ) {
162+ // This is only possible if the parent was missing from the lookup... Never supposed to happen.
150163 throw new Error ( `Invalid orphan hierarchical node: ${ existingData . id } ` )
151164 }
152165 const siblings = parent . children
@@ -159,13 +172,61 @@ function updateHierarchicalWorkflow (existingData, lookup, tree, options) {
159172 ( n ) => n . name ,
160173 sortWorkflowNamePartNodeOrWorkflowNode
161174 )
162- // If it is not where it is , we need to add it to its correct location.
175+ // If it is not where it must be , we need to move it to its correct location.
163176 if ( currentIndex !== sortedIndex ) {
177+ // The two lines above were my first try, but the UI appeared to render really slowly?
164178 // siblings.splice(currentIndex, 1)
165179 // siblings.splice(sortedIndex, 0, existingData)
166180 Vue . delete ( siblings , currentIndex )
167181 Vue . set ( siblings , sortedIndex , existingData )
168182 }
183+ // Finally, we need to propagate the state totals and latest state tasks,
184+ // but only if we have a parent (otherwise we are at the top-most level).
185+ if ( parentId ) {
186+ const workflow = lookup [ workflowId ]
187+ const latestStateTasks = workflow . node . latestStateTasks
188+ const stateTotals = workflow . node . stateTotals
189+ // Installed workflows do not have any state.
190+ if ( latestStateTasks && stateTotals ) {
191+ for ( const parentNodeId of [ ...nodesIds , parentId ] ) {
192+ const parentNode = lookup [ parentNodeId ]
193+ if ( parentNode . latestStateTasks && parentNode . stateTotals ) {
194+ mergeWith ( parentNode . node . descendantsLatestStateTasks [ workflow . id ] , latestStateTasks , mergeWithCustomizer )
195+ mergeWith ( parentNode . node . descendantsStateTotal [ workflow . id ] , stateTotals , mergeWithCustomizer )
196+ tallyPropagatedStates ( parentNode . node )
197+ }
198+ }
199+ }
200+ }
201+ }
202+
203+ /**
204+ * Computes the latestStateTasks of each node. The latestStateTasks and
205+ * stateTotals of a workflow-name-part are not reactive, but are calculated
206+ * based on the values of descendantsLatestStateTasks and descendantsStateTotal,
207+ * so we need to keep these in sync any time we add or update descendants.
208+ *
209+ * @param {WorkflowGraphQLData } node
210+ */
211+ function tallyPropagatedStates ( node ) {
212+ for ( const latestStateTasks of Object . values ( node . descendantsLatestStateTasks ) ) {
213+ for ( const state of Object . keys ( latestStateTasks ) ) {
214+ if ( node . latestStateTasks [ state ] ) {
215+ node . latestStateTasks [ state ] . push ( ...latestStateTasks [ state ] )
216+ } else {
217+ Vue . set ( node . latestStateTasks , state , latestStateTasks [ state ] )
218+ }
219+ }
220+ }
221+ for ( const stateTotals of Object . values ( node . descendantsStateTotal ) ) {
222+ for ( const state of Object . keys ( stateTotals ) ) {
223+ if ( node . stateTotals [ state ] ) {
224+ node . stateTotals [ state ] += stateTotals [ state ]
225+ } else {
226+ Vue . set ( node . stateTotals , state , stateTotals [ state ] )
227+ }
228+ }
229+ }
169230}
170231
171232// -- Pruned
@@ -205,21 +266,32 @@ function removeHierarchicalWorkflow (workflowId, lookup, tree, options) {
205266 const workflowNameParts = parseWorkflowNameParts ( workflowId )
206267 const nodesIds = getWorkflowNamePartsNodesIds ( workflowNameParts )
207268 // We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
269+ const removedNodeIds = [ ]
208270 for ( let i = nodesIds . length - 1 ; i >= 0 ; i -- ) {
209271 const nodeId = nodesIds [ i ]
210272 const node = lookup [ nodeId ]
273+ // If we have children nodes, we MUST not remove the node from the GScan tree, since
274+ // it contains other workflows branches. Instead, we must only remove the propagated
275+ // states.
211276 if ( node . children && node . children . length > 0 ) {
212- // We stop as soon as we find a node that still has children.
213- break
214- }
215- // Now we can remove the node from the lookup, and from its parents children array.
216- const previousIndex = i - 1
217- const parentId = previousIndex >= 0 ? nodesIds [ previousIndex ] : null
218- if ( parentId && ! lookup [ parentId ] ) {
219- throw new Error ( `Failed to locate parent ${ parentId } in GScan lookup` )
277+ // If we pruned a workflow that was installed, these states are undefined!
278+ if ( node . node . descendantsLatestStateTasks && node . node . descendantsStateTotal ) {
279+ for ( const removedNodeId of removedNodeIds ) {
280+ Vue . delete ( node . node . descendantsLatestStateTasks , removedNodeId )
281+ Vue . delete ( node . node . descendantsStateTotal , removedNodeId )
282+ }
283+ }
284+ } else {
285+ // Now we can remove the node from the lookup, and from its parents children array.
286+ const previousIndex = i - 1
287+ const parentId = previousIndex >= 0 ? nodesIds [ previousIndex ] : null
288+ if ( parentId && ! lookup [ parentId ] ) {
289+ throw new Error ( `Failed to locate parent ${ parentId } in GScan lookup` )
290+ }
291+ const parentChildren = parentId ? lookup [ parentId ] . children : tree
292+ removeNode ( nodeId , lookup , parentChildren )
293+ removedNodeIds . push ( nodeId )
220294 }
221- const parentChildren = parentId ? lookup [ parentId ] . children : tree
222- removeNode ( nodeId , lookup , parentChildren )
223295 }
224296}
225297
0 commit comments