Skip to content

Commit

Permalink
Update and delete GScan tree and lookup nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
kinow committed Sep 16, 2021
1 parent 44125c0 commit dd1c293
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 84 deletions.
85 changes: 81 additions & 4 deletions src/components/cylc/gscan/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { mergeWith } from 'lodash'
import { sortedIndexBy } from '@/components/cylc/common/sort'
import { mergeWithCustomizer } from '@/components/cylc/common/merge'
import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/sort'
import { createWorkflowNode } from '@/components/cylc/gscan/nodes'
import {
createWorkflowNode,
getWorkflowNamePartsNodesIds,
parseWorkflowNameParts
} from '@/components/cylc/gscan/nodes'

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

// --- Added

/**
* Add a new workflow to the GScan data structure.
*
* @param {TreeNode} workflow
* @param {GScan} gscan
* @param {*} options
Expand Down Expand Up @@ -89,6 +97,7 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
// TODO: combine states summaries?
if (existingNode.children) {
// Copy array since we will iterate it, and modify existingNode.children
// (see the tree.splice above.)
const children = [...workflow.children]
for (const child of children) {
// Recursion
Expand All @@ -101,10 +110,14 @@ function addHierarchicalWorkflow (workflow, lookup, tree, options) {
}
}

// --- Updated

/**
* Update a workflow in the GScan data structure.
*
* @param {WorkflowGraphQLData} workflow
* @param {GScan} gscan
* @param {*} options
* @param {Object} options
*/
function updateWorkflow (workflow, gscan, options) {
// We don't care whether it is hierarchical or not here, since we can quickly
Expand All @@ -115,15 +128,79 @@ function updateWorkflow (workflow, gscan, options) {
}
mergeWith(existingData.node, workflow, mergeWithCustomizer)
Vue.set(gscan.lookup, existingData.id, existingData)
// FIXME: we need to sort its parent again!
// TODO: create workflow hierarchy (from workflow object), then iterate
// it and use lookup to fetch the existing node. Finally, combine
// the gscan states (latestStateTasks & stateTotals).
}

// -- Pruned

/**
* @param {TreeNode} workflow
* Remove the workflow with ID equals to the given `workflowId` from the GScan data structure.
*
* @param {String} workflowId
* @param {GScan} gscan
* @param {*} options
*/
function removeWorkflow (workflow, gscan, options) {
function removeWorkflow (workflowId, gscan, options) {
const workflow = gscan.lookup[workflowId]
if (!workflow) {
throw new Error(`Pruned node [${workflow.id}] not found in workflow lookup`)
}
const hierarchical = options.hierarchical || true
if (hierarchical) {
removeHierarchicalWorkflow(workflowId, gscan.lookup, gscan.tree, options)
} else {
removeNode(workflowId, gscan.lookup, gscan.tree)
}
}

/**
* This function is private. It removes the workflow associated with the given `workflowId` from the
* lookup, and also proceeds to remove the leaf-node with the workflow node, and all of its parents that
* do not have any other descendants.
*
* @param {String} workflowId - Existing workflow ID
* @param {Lookup} lookup
* @param {Array<TreeNode>} tree
* @param {Object} options
* @private
*/
function removeHierarchicalWorkflow (workflowId, lookup, tree, options) {
const workflowNameParts = parseWorkflowNameParts(workflowId)
const nodesIds = getWorkflowNamePartsNodesIds(workflowNameParts)
// We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
for (let i = nodesIds.length - 1; i >= 0; i--) {
const nodeId = nodesIds[i]
const node = lookup[nodeId]
if (node.children && node.children.length > 0) {
// We stop as soon as we find a node that still has children.
break
}
// Now we can remove the node from the lookup, and from its parents children array.
const previousIndex = i - 1
const parentId = previousIndex >= 0 ? nodesIds[previousIndex] : null
if (parentId && !lookup[parentId]) {
throw new Error(`Failed to locate parent ${parentId} in GScan lookup`)
}
const parentChildren = parentId ? lookup[parentId].children : tree
removeNode(nodeId, lookup, parentChildren)
}
}

/**
* @param {String} id - ID of the tree node
* @param {Array<TreeNode>} tree
* @param {Lookup} lookup
* @private
*/
function removeNode (id, lookup, tree) {
Vue.delete(lookup, id)
const treeNode = tree.find(node => node.id === id)
if (treeNode) {
Vue.delete(tree, tree.indexOf(treeNode))
}
}

export {
Expand Down
194 changes: 114 additions & 80 deletions src/components/cylc/gscan/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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

/**
* @typedef {Object} TreeNode
* @property {String} id
* @property {String} name
* @property {String|null} name
* @property {String} type
* @property {WorkflowGraphQLData} node
*/

/**
* @typedef {TreeNode} WorkflowGScanNode
*/
Expand All @@ -34,12 +36,60 @@ import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/
* @property {Array<WorkflowNamePartGScanNode>} children
*/

/**
* Create a workflow node for GScan component.
*
* If the `hierarchy` parameter is `true`, then the workflow name will be split by
* `/`'s. For each part, a new `WorkflowNamePart` will be added in the hierarchy.
* With the final node being the last part of the name.
*
* The last part of a workflow name may be the workflow name (e.g. `five`), or its
* run ID (e.g. `run1`, if workflow name is `five/run1`).
*
* @param {WorkflowGraphQLData} workflow
* @param {boolean} hierarchy - whether to parse the Workflow name and create a hierarchy or not
* @param {String} partsSeparator - separator for workflow name parts (e.g. '|' as in 'part1|part2|...')
* @param {String} namesSeparator - separator used for workflow and run names (e.g. '/' as in 'workflow/run1')
* @returns {TreeNode|null}
*/
function createWorkflowNode (workflow, hierarchy, partsSeparator = DEFAULT_PARTS_SEPARATOR, namesSeparator = DEFAULT_NAMES_SEPARATOR) {
if (!hierarchy) {
return newWorkflowNode(workflow, null)
}
const workflowNameParts = parseWorkflowNameParts(workflow.id, partsSeparator, namesSeparator)
let prefix = workflowNameParts.user
// The root node, returned in this function.
let rootNode = null
// And a helper used when iterating the array...
let currentNode = null
for (const part of workflowNameParts.parts) {
prefix = prefix === null ? part : `${prefix}${partsSeparator}${part}`
const partNode = newWorkflowPartNode(prefix, part)
if (rootNode === null) {
rootNode = currentNode = partNode
} else {
currentNode.children.push(partNode)
currentNode = partNode
}
}
const workflowNode = newWorkflowNode(workflow, workflowNameParts.name)

if (currentNode === null) {
// We will return the workflow node only. It will be appended directly to the tree as a new leaf.
rootNode = workflowNode
} else {
// 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.
currentNode.children.push(workflowNode)
}
return rootNode
}

/**
* Create a new Workflow Node.
*
* @private
* @param {WorkflowGraphQLData} workflow
* @param {string|null} part
* @param {String|null} part
* @returns {WorkflowGScanNode}
*/
function newWorkflowNode (workflow, part) {
Expand All @@ -60,11 +110,11 @@ function newWorkflowNode (workflow, part) {
*/
function newWorkflowPartNode (id, part) {
return {
id: `workflow-name-part-${id}`,
id,
name: part,
type: 'workflow-name-part',
node: {
id: id,
id,
name: part,
status: ''
},
Expand All @@ -73,90 +123,74 @@ function newWorkflowPartNode (id, part) {
}

/**
* Create a workflow node for GScan component.
*
* If the `hierarchy` parameter is `true`, then the workflow name will be split by
* `/`'s. For each part, a new `WorkflowNamePart` will be added in the hierarchy.
* With the final node being the last part of the name.
*
* The last part of a workflow name may be the workflow name (e.g. `five`), or its
* run ID (e.g. `run1`, if workflow name is `five/run1`).
*
* @param {WorkflowGraphQLData} workflow
* @param {boolean} hierarchy - whether to parse the Workflow name and create a hierarchy or not
* @returns {TreeNode|null}
* @typedef {Object} ParsedWorkflowNameParts
* @property {String} workflowId - workflow ID
* @property {String} partsSeparator - parts separator parameter used to parse the name
* @property {String} namesSeparator - names separator parameter used to parse the name
* @property {String} user - parsed workflow user/owner
* @property {String} workflowName - original workflow name
* @property {Array<String>} parts - workflow name parts
* @property {String} name - workflow name (last part, used to display nodes in the GScan tree)
*/
function createWorkflowNode (workflow, hierarchy) {
if (!hierarchy) {
return newWorkflowNode(workflow, null)
}
const workflowIdParts = workflow.id.split('|')
// The prefix contains all the ID parts, except for the workflow name.
let prefix = workflowIdParts.slice(0, workflowIdParts.length - 1)
// The name is here.
const workflowName = workflow.name
const parts = workflowName.split('/')
// Returned node...
let rootNode = null
// And a helper used when iterating the array...
let currentNode = null
while (parts.length > 0) {
const part = parts.shift()
// For the first part, we need to add an ID separator `|`, but for the other parts
// we actually want to use the name parts separator `/`.
prefix = prefix.includes('/') ? `${prefix}/${part}` : `${prefix}|${part}`
const partNode = parts.length !== 0
? newWorkflowPartNode(prefix, part)
: newWorkflowNode(workflow, part)

if (rootNode === null) {
rootNode = currentNode = partNode
} else {
currentNode.children.push(partNode)
currentNode = partNode
}
}
return rootNode
/**
* Return the workflow name parts as an array of node IDs. The first node in the array is the top of the
* branch, with the workflow node ID at its other end, as a leaf-node.
*
* @param {ParsedWorkflowNameParts} workflowNameParts
* @return {Array<String>}
*/
function getWorkflowNamePartsNodesIds (workflowNameParts) {
let prefix = workflowNameParts.user
const nodesIds = workflowNameParts.parts
.map(part => {
prefix = `${prefix}${workflowNameParts.partsSeparator}${part}`
return prefix
})
nodesIds.push(workflowNameParts.workflowId)
}

/**
* Add the new hierarchical node to the list of existing nodes.
* Parses the workflow name parts. A simple name such as `user|workflow-name` will return a structure
* with each part of the name, including the given parameters of this function (to simplify sending
* the data to other methods).
*
* New nodes are added in order.
* More complicated names such as `user|top/level/other/leaf` return the structure with an array of
* each name part too. This is useful for functions that need to manipulate the tree of GScan nodes,
* and necessary as we don't have this information from the server (only the name which doesn't
* split the name parts).
*
* @param {WorkflowGScanNode|WorkflowNamePartGScanNode} node
* @param {Array<WorkflowGScanNode|WorkflowNamePartGScanNode>} nodes
* @return {Array<WorkflowGScanNode|WorkflowNamePartGScanNode>}
* @param {String} workflowId - Workflow ID
* @param {String} partsSeparator - separator for workflow name parts (e.g. '|' as in 'user|research/workflow/run1')
* @param {String} namesSeparator - separator used for workflow and run names (e.g. '/' as in 'research/workflow/run1')
* @return {ParsedWorkflowNameParts}
*/
function addNodeToTree (node, nodes) {
// N.B.: We must compare nodes by ID, not only by part-name,
// since we can have research/nwp/run1 workflow, and research workflow;
// in this case we do not want to confuse the research part-name with
// the research workflow.
const existingNode = nodes.find((existingNode) => existingNode.id === node.id)
if (!existingNode) {
// Here we calculate what is the index for this element. If we decide to have ASC and DESC,
// then we just need to invert the location of the element, something like
// `sortedIndex = (array.length - sortedIndex)`.
const sortedIndex = sortedIndexBy(
nodes,
node,
(n) => n.name,
sortWorkflowNamePartNodeOrWorkflowNode
)
nodes.splice(sortedIndex, 0, node)
} else {
if (node.children) {
for (const child of node.children) {
// Recursion. Note that we are changing the `nodes` to the children of the existing node.
addNodeToTree(child, existingNode.children)
}
}
function parseWorkflowNameParts (workflowId, partsSeparator = DEFAULT_PARTS_SEPARATOR, namesSeparator = DEFAULT_NAMES_SEPARATOR) {
if (!workflowId || workflowId.trim() === '') {
throw new Error('Missing ID for workflow name parts')
}
const idParts = workflowId.split(partsSeparator)
if (idParts.length !== 2) {
throw new Error(`Invalid parts found, expected at least 2 parts in ${workflowId}`)
}
const user = idParts[0]
const workflowName = idParts[1]
const parts = workflowName.split(namesSeparator)
// The name, used for display in the tree. Can be a workflow name like 'd', or a runN like 'run1'.
const name = parts.pop()
return {
workflowId,
partsSeparator,
namesSeparator,
user, // user
workflowName, // a/b/c/d/run1
parts, // [a, b, c, d]
name // run1
}
return nodes
}

export {
addNodeToTree,
createWorkflowNode
createWorkflowNode,
getWorkflowNamePartsNodesIds,
parseWorkflowNameParts
}

0 comments on commit dd1c293

Please sign in to comment.