Skip to content

Commit 3a7be22

Browse files
committed
Update and delete GScan tree and lookup nodes
1 parent 4ee908d commit 3a7be22

File tree

2 files changed

+195
-84
lines changed

2 files changed

+195
-84
lines changed

src/components/cylc/gscan/index.js

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import { mergeWith } from 'lodash'
1919
import { sortedIndexBy } from '@/components/cylc/common/sort'
2020
import { mergeWithCustomizer } from '@/components/cylc/common/merge'
2121
import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/sort'
22-
import { createWorkflowNode } from '@/components/cylc/gscan/nodes'
22+
import {
23+
createWorkflowNode,
24+
getWorkflowNamePartsNodesIds,
25+
parseWorkflowNameParts
26+
} from '@/components/cylc/gscan/nodes'
2327

2428
/**
2529
* @typedef {Object} GScan
@@ -31,7 +35,11 @@ import { createWorkflowNode } from '@/components/cylc/gscan/nodes'
3135
* @typedef {Object<String, TreeNode>} Lookup
3236
*/
3337

38+
// --- Added
39+
3440
/**
41+
* Add a new workflow to the GScan data structure.
42+
*
3543
* @param {TreeNode} workflow
3644
* @param {GScan} gscan
3745
* @param {*} options
@@ -89,6 +97,7 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
8997
// TODO: combine states summaries?
9098
if (existingNode.children) {
9199
// Copy array since we will iterate it, and modify existingNode.children
100+
// (see the tree.splice above.)
92101
const children = [...workflow.children]
93102
for (const child of children) {
94103
// Recursion
@@ -101,10 +110,14 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
101110
}
102111
}
103112

113+
// --- Updated
114+
104115
/**
116+
* Update a workflow in the GScan data structure.
117+
*
105118
* @param {WorkflowGraphQLData} workflow
106119
* @param {GScan} gscan
107-
* @param {*} options
120+
* @param {Object} options
108121
*/
109122
function updateWorkflow (workflow, gscan, options) {
110123
// We don't care whether it is hierarchical or not here, since we can quickly
@@ -115,15 +128,79 @@ function updateWorkflow (workflow, gscan, options) {
115128
}
116129
mergeWith(existingData.node, workflow, mergeWithCustomizer)
117130
Vue.set(gscan.lookup, existingData.id, existingData)
131+
// FIXME: we need to sort its parent again!
132+
// TODO: create workflow hierarchy (from workflow object), then iterate
133+
// it and use lookup to fetch the existing node. Finally, combine
134+
// the gscan states (latestStateTasks & stateTotals).
118135
}
119136

137+
// -- Pruned
138+
120139
/**
121-
* @param {TreeNode} workflow
140+
* Remove the workflow with ID equals to the given `workflowId` from the GScan data structure.
141+
*
142+
* @param {String} workflowId
122143
* @param {GScan} gscan
123144
* @param {*} options
124145
*/
125-
function removeWorkflow (workflow, gscan, options) {
146+
function removeWorkflow (workflowId, gscan, options) {
147+
const workflow = gscan.lookup[workflowId]
148+
if (!workflow) {
149+
throw new Error(`Pruned node [${workflow.id}] not found in workflow lookup`)
150+
}
151+
const hierarchical = options.hierarchical || true
152+
if (hierarchical) {
153+
removeHierarchicalWorkflow(workflowId, gscan.lookup, gscan.tree, options)
154+
} else {
155+
removeNode(workflowId, gscan.lookup, gscan.tree)
156+
}
157+
}
126158

159+
/**
160+
* This function is private. It removes the workflow associated with the given `workflowId` from the
161+
* lookup, and also proceeds to remove the leaf-node with the workflow node, and all of its parents that
162+
* do not have any other descendants.
163+
*
164+
* @param {String} workflowId - Existing workflow ID
165+
* @param {Lookup} lookup
166+
* @param {Array<TreeNode>} tree
167+
* @param {Object} options
168+
* @private
169+
*/
170+
function removeHierarchicalWorkflow (workflowId, lookup, tree, options) {
171+
const workflowNameParts = parseWorkflowNameParts(workflowId)
172+
const nodesIds = getWorkflowNamePartsNodesIds(workflowNameParts)
173+
// We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
174+
for (let i = nodesIds.length - 1; i >= 0; i--) {
175+
const nodeId = nodesIds[i]
176+
const node = lookup[nodeId]
177+
if (node.children && node.children.length > 0) {
178+
// We stop as soon as we find a node that still has children.
179+
break
180+
}
181+
// Now we can remove the node from the lookup, and from its parents children array.
182+
const previousIndex = i - 1
183+
const parentId = previousIndex >= 0 ? nodesIds[previousIndex] : null
184+
if (parentId && !lookup[parentId]) {
185+
throw new Error(`Failed to locate parent ${parentId} in GScan lookup`)
186+
}
187+
const parentChildren = parentId ? lookup[parentId].children : tree
188+
removeNode(nodeId, lookup, parentChildren)
189+
}
190+
}
191+
192+
/**
193+
* @param {String} id - ID of the tree node
194+
* @param {Array<TreeNode>} tree
195+
* @param {Lookup} lookup
196+
* @private
197+
*/
198+
function removeNode (id, lookup, tree) {
199+
Vue.delete(lookup, id)
200+
const treeNode = tree.find(node => node.id === id)
201+
if (treeNode) {
202+
Vue.delete(tree, tree.indexOf(treeNode))
203+
}
127204
}
128205

129206
export {

src/components/cylc/gscan/nodes.js

Lines changed: 114 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@
1515
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
*/
1717

18-
import { sortedIndexBy } from '@/components/cylc/common/sort'
19-
import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/sort'
18+
// TODO: move to the `options` parameter that is passed to deltas; ideally it would be stored in DB or localstorage.
19+
const DEFAULT_PARTS_SEPARATOR = '|'
20+
const DEFAULT_NAMES_SEPARATOR = '/'
2021

2122
/**
2223
* @typedef {Object} TreeNode
2324
* @property {String} id
24-
* @property {String} name
25+
* @property {String|null} name
2526
* @property {String} type
2627
* @property {WorkflowGraphQLData} node
2728
*/
29+
2830
/**
2931
* @typedef {TreeNode} WorkflowGScanNode
3032
*/
@@ -34,12 +36,60 @@ import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/
3436
* @property {Array<WorkflowNamePartGScanNode>} children
3537
*/
3638

39+
/**
40+
* Create a workflow node for GScan component.
41+
*
42+
* If the `hierarchy` parameter is `true`, then the workflow name will be split by
43+
* `/`'s. For each part, a new `WorkflowNamePart` will be added in the hierarchy.
44+
* With the final node being the last part of the name.
45+
*
46+
* The last part of a workflow name may be the workflow name (e.g. `five`), or its
47+
* run ID (e.g. `run1`, if workflow name is `five/run1`).
48+
*
49+
* @param {WorkflowGraphQLData} workflow
50+
* @param {boolean} hierarchy - whether to parse the Workflow name and create a hierarchy or not
51+
* @param {String} partsSeparator - separator for workflow name parts (e.g. '|' as in 'part1|part2|...')
52+
* @param {String} namesSeparator - separator used for workflow and run names (e.g. '/' as in 'workflow/run1')
53+
* @returns {TreeNode|null}
54+
*/
55+
function createWorkflowNode (workflow, hierarchy, partsSeparator = DEFAULT_PARTS_SEPARATOR, namesSeparator = DEFAULT_NAMES_SEPARATOR) {
56+
if (!hierarchy) {
57+
return newWorkflowNode(workflow, null)
58+
}
59+
const workflowNameParts = parseWorkflowNameParts(workflow.id, partsSeparator, namesSeparator)
60+
let prefix = workflowNameParts.user
61+
// The root node, returned in this function.
62+
let rootNode = null
63+
// And a helper used when iterating the array...
64+
let currentNode = null
65+
for (const part of workflowNameParts.parts) {
66+
prefix = prefix === null ? part : `${prefix}${partsSeparator}${part}`
67+
const partNode = newWorkflowPartNode(prefix, part)
68+
if (rootNode === null) {
69+
rootNode = currentNode = partNode
70+
} else {
71+
currentNode.children.push(partNode)
72+
currentNode = partNode
73+
}
74+
}
75+
const workflowNode = newWorkflowNode(workflow, workflowNameParts.name)
76+
77+
if (currentNode === null) {
78+
// We will return the workflow node only. It will be appended directly to the tree as a new leaf.
79+
rootNode = workflowNode
80+
} else {
81+
// Add the workflow node to the end of the branch as a leaf. Note that the top of the branch is returned in this case.
82+
currentNode.children.push(workflowNode)
83+
}
84+
return rootNode
85+
}
86+
3787
/**
3888
* Create a new Workflow Node.
3989
*
4090
* @private
4191
* @param {WorkflowGraphQLData} workflow
42-
* @param {string|null} part
92+
* @param {String|null} part
4393
* @returns {WorkflowGScanNode}
4494
*/
4595
function newWorkflowNode (workflow, part) {
@@ -60,11 +110,11 @@ function newWorkflowNode (workflow, part) {
60110
*/
61111
function newWorkflowPartNode (id, part) {
62112
return {
63-
id: `workflow-name-part-${id}`,
113+
id,
64114
name: part,
65115
type: 'workflow-name-part',
66116
node: {
67-
id: id,
117+
id,
68118
name: part,
69119
status: ''
70120
},
@@ -73,90 +123,74 @@ function newWorkflowPartNode (id, part) {
73123
}
74124

75125
/**
76-
* Create a workflow node for GScan component.
77-
*
78-
* If the `hierarchy` parameter is `true`, then the workflow name will be split by
79-
* `/`'s. For each part, a new `WorkflowNamePart` will be added in the hierarchy.
80-
* With the final node being the last part of the name.
81-
*
82-
* The last part of a workflow name may be the workflow name (e.g. `five`), or its
83-
* run ID (e.g. `run1`, if workflow name is `five/run1`).
84-
*
85-
* @param {WorkflowGraphQLData} workflow
86-
* @param {boolean} hierarchy - whether to parse the Workflow name and create a hierarchy or not
87-
* @returns {TreeNode|null}
126+
* @typedef {Object} ParsedWorkflowNameParts
127+
* @property {String} workflowId - workflow ID
128+
* @property {String} partsSeparator - parts separator parameter used to parse the name
129+
* @property {String} namesSeparator - names separator parameter used to parse the name
130+
* @property {String} user - parsed workflow user/owner
131+
* @property {String} workflowName - original workflow name
132+
* @property {Array<String>} parts - workflow name parts
133+
* @property {String} name - workflow name (last part, used to display nodes in the GScan tree)
88134
*/
89-
function createWorkflowNode (workflow, hierarchy) {
90-
if (!hierarchy) {
91-
return newWorkflowNode(workflow, null)
92-
}
93-
const workflowIdParts = workflow.id.split('|')
94-
// The prefix contains all the ID parts, except for the workflow name.
95-
let prefix = workflowIdParts.slice(0, workflowIdParts.length - 1)
96-
// The name is here.
97-
const workflowName = workflow.name
98-
const parts = workflowName.split('/')
99-
// Returned node...
100-
let rootNode = null
101-
// And a helper used when iterating the array...
102-
let currentNode = null
103-
while (parts.length > 0) {
104-
const part = parts.shift()
105-
// For the first part, we need to add an ID separator `|`, but for the other parts
106-
// we actually want to use the name parts separator `/`.
107-
prefix = prefix.includes('/') ? `${prefix}/${part}` : `${prefix}|${part}`
108-
const partNode = parts.length !== 0
109-
? newWorkflowPartNode(prefix, part)
110-
: newWorkflowNode(workflow, part)
111135

112-
if (rootNode === null) {
113-
rootNode = currentNode = partNode
114-
} else {
115-
currentNode.children.push(partNode)
116-
currentNode = partNode
117-
}
118-
}
119-
return rootNode
136+
/**
137+
* Return the workflow name parts as an array of node IDs. The first node in the array is the top of the
138+
* branch, with the workflow node ID at its other end, as a leaf-node.
139+
*
140+
* @param {ParsedWorkflowNameParts} workflowNameParts
141+
* @return {Array<String>}
142+
*/
143+
function getWorkflowNamePartsNodesIds (workflowNameParts) {
144+
let prefix = workflowNameParts.user
145+
const nodesIds = workflowNameParts.parts
146+
.map(part => {
147+
prefix = `${prefix}${workflowNameParts.partsSeparator}${part}`
148+
return prefix
149+
})
150+
nodesIds.push(workflowNameParts.workflowId)
120151
}
121152

122153
/**
123-
* Add the new hierarchical node to the list of existing nodes.
154+
* Parses the workflow name parts. A simple name such as `user|workflow-name` will return a structure
155+
* with each part of the name, including the given parameters of this function (to simplify sending
156+
* the data to other methods).
124157
*
125-
* New nodes are added in order.
158+
* More complicated names such as `user|top/level/other/leaf` return the structure with an array of
159+
* each name part too. This is useful for functions that need to manipulate the tree of GScan nodes,
160+
* and necessary as we don't have this information from the server (only the name which doesn't
161+
* split the name parts).
126162
*
127-
* @param {WorkflowGScanNode|WorkflowNamePartGScanNode} node
128-
* @param {Array<WorkflowGScanNode|WorkflowNamePartGScanNode>} nodes
129-
* @return {Array<WorkflowGScanNode|WorkflowNamePartGScanNode>}
163+
* @param {String} workflowId - Workflow ID
164+
* @param {String} partsSeparator - separator for workflow name parts (e.g. '|' as in 'user|research/workflow/run1')
165+
* @param {String} namesSeparator - separator used for workflow and run names (e.g. '/' as in 'research/workflow/run1')
166+
* @return {ParsedWorkflowNameParts}
130167
*/
131-
function addNodeToTree (node, nodes) {
132-
// N.B.: We must compare nodes by ID, not only by part-name,
133-
// since we can have research/nwp/run1 workflow, and research workflow;
134-
// in this case we do not want to confuse the research part-name with
135-
// the research workflow.
136-
const existingNode = nodes.find((existingNode) => existingNode.id === node.id)
137-
if (!existingNode) {
138-
// Here we calculate what is the index for this element. If we decide to have ASC and DESC,
139-
// then we just need to invert the location of the element, something like
140-
// `sortedIndex = (array.length - sortedIndex)`.
141-
const sortedIndex = sortedIndexBy(
142-
nodes,
143-
node,
144-
(n) => n.name,
145-
sortWorkflowNamePartNodeOrWorkflowNode
146-
)
147-
nodes.splice(sortedIndex, 0, node)
148-
} else {
149-
if (node.children) {
150-
for (const child of node.children) {
151-
// Recursion. Note that we are changing the `nodes` to the children of the existing node.
152-
addNodeToTree(child, existingNode.children)
153-
}
154-
}
168+
function parseWorkflowNameParts (workflowId, partsSeparator = DEFAULT_PARTS_SEPARATOR, namesSeparator = DEFAULT_NAMES_SEPARATOR) {
169+
if (!workflowId || workflowId.trim() === '') {
170+
throw new Error('Missing ID for workflow name parts')
171+
}
172+
const idParts = workflowId.split(partsSeparator)
173+
if (idParts.length !== 2) {
174+
throw new Error(`Invalid parts found, expected at least 2 parts in ${workflowId}`)
175+
}
176+
const user = idParts[0]
177+
const workflowName = idParts[1]
178+
const parts = workflowName.split(namesSeparator)
179+
// The name, used for display in the tree. Can be a workflow name like 'd', or a runN like 'run1'.
180+
const name = parts.pop()
181+
return {
182+
workflowId,
183+
partsSeparator,
184+
namesSeparator,
185+
user, // user
186+
workflowName, // a/b/c/d/run1
187+
parts, // [a, b, c, d]
188+
name // run1
155189
}
156-
return nodes
157190
}
158191

159192
export {
160-
addNodeToTree,
161-
createWorkflowNode
193+
createWorkflowNode,
194+
getWorkflowNamePartsNodesIds,
195+
parseWorkflowNameParts
162196
}

0 commit comments

Comments
 (0)