Skip to content

Commit fc4c584

Browse files
committed
WIP propagate task states (latestStateTasks and stateTotals)
1 parent d552e90 commit fc4c584

File tree

3 files changed

+142
-53
lines changed

3 files changed

+142
-53
lines changed

src/components/cylc/gscan/index.js

Lines changed: 124 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,21 @@ import {
4646
*/
4747
function 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,42 +145,92 @@ 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

140154
function 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
153166
// Where is this node at the moment?
154167
const currentIndex = siblings.findIndex(node => node.id === existingData.id)
155168
// Where should it be now?
156169
const sortedIndex = sortedIndexBy(
157-
parent.children,
170+
siblings,
158171
existingData,
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) {
164-
// siblings.splice(currentIndex, 1)
165-
// siblings.splice(sortedIndex, 0, existingData)
166-
Vue.delete(siblings, currentIndex)
167-
Vue.set(siblings, sortedIndex, existingData)
177+
// First we remove the element from where it was.
178+
siblings.splice(currentIndex, 1)
179+
if (sortedIndex < currentIndex) {
180+
// Now, if we must move the element to a position that is less than where it was, we can simply move it;
181+
siblings.splice(sortedIndex, 0, existingData)
182+
} else {
183+
// however, if we are moving it down/later, we must compensate for itself. i.e. the sortedIndex is considering
184+
// the element itself. So we subtract one from its position.
185+
siblings.splice(sortedIndex - 1, 0, existingData)
186+
}
187+
}
188+
// Finally, we need to propagate the state totals and latest state tasks,
189+
// but only if we have a parent (otherwise we are at the top-most level).
190+
if (parentId) {
191+
const workflow = lookup[workflowId]
192+
const latestStateTasks = workflow.node.latestStateTasks
193+
const stateTotals = workflow.node.stateTotals
194+
// Installed workflows do not have any state.
195+
if (latestStateTasks && stateTotals) {
196+
for (const parentNodeId of [...nodesIds, parentId]) {
197+
const parentNode = lookup[parentNodeId]
198+
if (parentNode.latestStateTasks && parentNode.stateTotals) {
199+
mergeWith(parentNode.node.descendantsLatestStateTasks[workflow.id], latestStateTasks, mergeWithCustomizer)
200+
mergeWith(parentNode.node.descendantsStateTotal[workflow.id], stateTotals, mergeWithCustomizer)
201+
tallyPropagatedStates(parentNode.node)
202+
}
203+
}
204+
}
205+
}
206+
}
207+
208+
/**
209+
* Computes the latestStateTasks of each node. The latestStateTasks and
210+
* stateTotals of a workflow-name-part are not reactive, but are calculated
211+
* based on the values of descendantsLatestStateTasks and descendantsStateTotal,
212+
* so we need to keep these in sync any time we add or update descendants.
213+
*
214+
* @param {WorkflowGraphQLData} node
215+
*/
216+
function tallyPropagatedStates (node) {
217+
for (const latestStateTasks of Object.values(node.descendantsLatestStateTasks)) {
218+
for (const state of Object.keys(latestStateTasks)) {
219+
if (node.latestStateTasks[state]) {
220+
node.latestStateTasks[state].push(...latestStateTasks[state])
221+
} else {
222+
Vue.set(node.latestStateTasks, state, latestStateTasks[state])
223+
}
224+
}
225+
}
226+
for (const stateTotals of Object.values(node.descendantsStateTotal)) {
227+
for (const state of Object.keys(stateTotals)) {
228+
if (node.stateTotals[state]) {
229+
Vue.set(node.stateTotals, state, node.stateTotals[state] + stateTotals[state])
230+
} else {
231+
Vue.set(node.stateTotals, state, stateTotals[state])
232+
}
233+
}
168234
}
169235
}
170236

@@ -205,21 +271,32 @@ function removeHierarchicalWorkflow (workflowId, lookup, tree, options) {
205271
const workflowNameParts = parseWorkflowNameParts(workflowId)
206272
const nodesIds = getWorkflowNamePartsNodesIds(workflowNameParts)
207273
// We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
274+
const removedNodeIds = []
208275
for (let i = nodesIds.length - 1; i >= 0; i--) {
209276
const nodeId = nodesIds[i]
210277
const node = lookup[nodeId]
278+
// If we have children nodes, we MUST not remove the node from the GScan tree, since
279+
// it contains other workflows branches. Instead, we must only remove the propagated
280+
// states.
211281
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`)
282+
// If we pruned a workflow that was installed, these states are undefined!
283+
if (node.node.descendantsLatestStateTasks && node.node.descendantsStateTotal) {
284+
for (const removedNodeId of removedNodeIds) {
285+
Vue.delete(node.node.descendantsLatestStateTasks, removedNodeId)
286+
Vue.delete(node.node.descendantsStateTotal, removedNodeId)
287+
}
288+
}
289+
} else {
290+
// Now we can remove the node from the lookup, and from its parents children array.
291+
const previousIndex = i - 1
292+
const parentId = previousIndex >= 0 ? nodesIds[previousIndex] : null
293+
if (parentId && !lookup[parentId]) {
294+
throw new Error(`Failed to locate parent ${parentId} in GScan lookup`)
295+
}
296+
const parentChildren = parentId ? lookup[parentId].children : tree
297+
removeNode(nodeId, lookup, parentChildren)
298+
removedNodeIds.push(nodeId)
220299
}
221-
const parentChildren = parentId ? lookup[parentId].children : tree
222-
removeNode(nodeId, lookup, parentChildren)
223300
}
224301
}
225302

src/components/cylc/gscan/nodes.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const DEFAULT_NAMES_SEPARATOR = '/'
5050
* @param {boolean} hierarchy - whether to parse the Workflow name and create a hierarchy or not
5151
* @param {String} partsSeparator - separator for workflow name parts (e.g. '|' as in 'part1|part2|...')
5252
* @param {String} namesSeparator - separator used for workflow and run names (e.g. '/' as in 'workflow/run1')
53-
* @returns {TreeNode|null}
53+
* @returns {TreeNode}
5454
*/
5555
function createWorkflowNode (workflow, hierarchy, partsSeparator = DEFAULT_PARTS_SEPARATOR, namesSeparator = DEFAULT_NAMES_SEPARATOR) {
5656
if (!hierarchy) {
@@ -64,7 +64,7 @@ function createWorkflowNode (workflow, hierarchy, partsSeparator = DEFAULT_PARTS
6464
let currentNode = null
6565
for (const part of workflowNameParts.parts) {
6666
prefix = prefix === null ? part : `${prefix}${partsSeparator}${part}`
67-
const partNode = newWorkflowPartNode(prefix, part)
67+
const partNode = newWorkflowPartNode(prefix, part, workflow.id, workflow.latestStateTasks, workflow.stateTotals)
6868
if (rootNode === null) {
6969
rootNode = currentNode = partNode
7070
} else {
@@ -87,10 +87,10 @@ function createWorkflowNode (workflow, hierarchy, partsSeparator = DEFAULT_PARTS
8787
/**
8888
* Create a new Workflow Node.
8989
*
90-
* @private
9190
* @param {WorkflowGraphQLData} workflow
9291
* @param {String|null} part
9392
* @returns {WorkflowGScanNode}
93+
* @private
9494
*/
9595
function newWorkflowNode (workflow, part) {
9696
return {
@@ -106,17 +106,29 @@ function newWorkflowNode (workflow, part) {
106106
*
107107
* @param {string} id
108108
* @param {string} part
109+
* @param {string} workflowId
110+
* @param {Object} latestStateTasks
111+
* @param {Object} stateTotals
109112
* @return {WorkflowNamePartGScanNode}
110113
*/
111-
function newWorkflowPartNode (id, part) {
114+
function newWorkflowPartNode (id, part, workflowId, latestStateTasks = [], stateTotals = []) {
112115
return {
113116
id,
114117
name: part,
115118
type: 'workflow-name-part',
116119
node: {
117120
id,
121+
workflowId,
118122
name: part,
119-
status: ''
123+
status: '',
124+
descendantsLatestStateTasks: {
125+
[workflowId]: latestStateTasks
126+
},
127+
descendantsStateTotal: {
128+
[workflowId]: stateTotals
129+
},
130+
latestStateTasks: {},
131+
stateTotals: {}
120132
},
121133
children: []
122134
}

src/services/mock/json/GscanSubscriptionQuery.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"workflow": {
3-
"id": "user|one",
3+
"id": "user|a/b/c/one",
44
"name": "one",
55
"status": "running",
66
"owner": "user",

0 commit comments

Comments
 (0)