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 10, 2021
1 parent a4d1f75 commit f3a81df
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 24 deletions.
63 changes: 59 additions & 4 deletions src/components/cylc/gscan/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ 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,
parseWorkflowNameParts
} from '@/components/cylc/gscan/nodes'

/**
* @typedef {Object} GScan
Expand Down Expand Up @@ -89,6 +92,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 @@ -115,15 +119,66 @@ 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).
}

/**
* @param {TreeNode} workflow
* @private
* @param {String} id - ID of the tree node
* @param {Array<TreeNode>} tree
* @param {Lookup} lookup
*/
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))
}
}

/**
* @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) {
const workflowNameParts = parseWorkflowNameParts(workflowId)
const nodeIds = []
let prefix = workflowNameParts.user
for (const part of workflowNameParts.parts) {
prefix = `${prefix}${workflowNameParts.partsSeparator}${part}`
nodeIds.push(prefix)
}
nodeIds.push(workflowId)
// We start from the leaf-node, going upward to make sure we don't leave nodes with no children.
for (let i = nodeIds.length - 1; i >= 0; i--) {
const nodeId = nodeIds[i]
const node = gscan.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 ? nodeIds[previousIndex] : null
if (parentId && !gscan.lookup[parentId]) {
throw new Error(`Failed to locate parent ${parentId} in GScan lookup`)
}
const parentChildren = parentId ? gscan.lookup[parentId].children : gscan.tree
removeNode(nodeId, gscan.lookup, parentChildren)
}
} else {
removeNode(workflowId, gscan.lookup, gscan.tree)
}
}

export {
Expand Down
74 changes: 54 additions & 20 deletions src/components/cylc/gscan/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
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
Expand Down Expand Up @@ -60,18 +64,46 @@ 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: ''
},
children: []
}
}

/**
* @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')
*/
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 {
partsSeparator,
namesSeparator,
user, // user
workflowName, // a/b/c/d/run1
parts, // [a, b, c, d]
name // run1
}
}

/**
* Create a workflow node for GScan component.
*
Expand All @@ -84,38 +116,39 @@ function newWorkflowPartNode (id, part) {
*
* @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) {
function createWorkflowNode (workflow, hierarchy, partsSeparator = DEFAULT_PARTS_SEPARATOR, namesSeparator = DEFAULT_NAMES_SEPARATOR) {
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...
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
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)

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
}

Expand Down Expand Up @@ -158,5 +191,6 @@ function addNodeToTree (node, nodes) {

export {
addNodeToTree,
createWorkflowNode
createWorkflowNode,
parseWorkflowNameParts
}

0 comments on commit f3a81df

Please sign in to comment.