diff --git a/src/components/cylc/gscan/index.js b/src/components/cylc/gscan/index.js index 3c4202be3f..517f3536bf 100644 --- a/src/components/cylc/gscan/index.js +++ b/src/components/cylc/gscan/index.js @@ -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 @@ -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 @@ -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} 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 { diff --git a/src/components/cylc/gscan/nodes.js b/src/components/cylc/gscan/nodes.js index 7909bbb83f..cf4000c61b 100644 --- a/src/components/cylc/gscan/nodes.js +++ b/src/components/cylc/gscan/nodes.js @@ -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 @@ -60,11 +64,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: '' }, @@ -72,6 +76,34 @@ function newWorkflowPartNode (id, part) { } } +/** + * @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. * @@ -84,31 +116,23 @@ 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 { @@ -116,6 +140,15 @@ function createWorkflowNode (workflow, hierarchy) { 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 } @@ -158,5 +191,6 @@ function addNodeToTree (node, nodes) { export { addNodeToTree, - createWorkflowNode + createWorkflowNode, + parseWorkflowNameParts }