diff --git a/src/components/cylc/common/deltas.js b/src/components/cylc/common/deltas.js index ef5a2f9ef..acc3c0641 100644 --- a/src/components/cylc/common/deltas.js +++ b/src/components/cylc/common/deltas.js @@ -15,6 +15,11 @@ * along with this program. If not, see . */ +import Vue from 'vue' +import mergeWith from 'lodash/mergeWith' +import isArray from 'lodash/isArray' +import { mergeWithCustomizer } from '@/components/cylc/common/merge' + /** * @typedef {Object} GraphQLResponseData * @property {Deltas} deltas @@ -79,3 +84,133 @@ * @property {Array} familyProxies - IDs of family proxies removed * @property {Array} jobs - IDs of jobs removed */ + +/** + * @typedef {Object} Result + * @property {Array} errors + */ + +/** + * @param {DeltasAdded} added + * @param {Object.} lookup + * @return {Result} + */ +function applyDeltasAdded (added, lookup) { + const result = { + errors: [] + } + Object.values(added).forEach(addedValue => { + const items = isArray(addedValue) ? addedValue : [addedValue] + items.forEach(addedData => { + // An example for a data without .id, is the empty delta with __typename: "Added". It does occur, and can cause runtime errors. + if (addedData.id) { + try { + Vue.set(lookup, addedData.id, addedData) + } catch (error) { + result.errors.push([ + 'Error applying added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', + error, + addedData, + lookup + ]) + } + } + }) + }) + return result +} + +/** + * Deltas updated. + * + * @param updated {DeltasUpdated} updated + * @param {Object.} lookup + * @return {Result} + */ +function applyDeltasUpdated (updated, lookup) { + const result = { + errors: [] + } + Object.values(updated).forEach(updatedValue => { + const items = isArray(updatedValue) ? updatedValue : [updatedValue] + items.forEach(updatedData => { + // An example for a data without .id, is the empty delta with __typename: "Updated". It does occur, and can cause runtime errors. + if (updatedData.id) { + try { + const existingNode = lookup[updatedData.id] + if (existingNode) { + mergeWith(existingNode, updatedData, mergeWithCustomizer) + } else { + // TODO: we are adding in the updated. Is that OK? Should we revisit it later perhaps? + Vue.set(lookup, updatedData.id, updatedData) + } + } catch (error) { + result.errors.push([ + 'Error applying updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', + error, + updatedData, + lookup + ]) + } + } + }) + }) + return result +} + +/** + * Deltas pruned. + * + * @param {DeltasPruned} pruned - deltas pruned + * @param {Object.} lookup + * @return {Result} + */ +function applyDeltasPruned (pruned, lookup) { + Object.values(pruned).forEach(prunedData => { + // It can be a String, when you get a delta with only '{ __typename: "Pruned" }' + const items = isArray(prunedData) ? prunedData : [prunedData] + for (const id of items) { + if (lookup[id]) { + delete lookup[id] + } + } + }) + return { + errors: [] + } +} + +/** + * A function that simply applies the deltas to a lookup object. + * + * The entries in deltas will be the value of the lookup, and their ID's + * will be the keys. + * + * This function can be used with any lookup-like structure. When + * entries are updated it will merge with a customizer maintaining + * the Vue reactivity. + * + * @param {GraphQLResponseData} data + * @param {Object.} lookup + */ +export default function (data, lookup) { + const added = data.deltas.added + const updated = data.deltas.updated + const pruned = data.deltas.pruned + const errors = [] + if (added) { + const result = applyDeltasAdded(added, lookup) + errors.push(...result.errors) + } + if (updated) { + const result = applyDeltasUpdated(updated, lookup) + errors.push(...result.errors) + } + if (pruned) { + const result = applyDeltasPruned(pruned, lookup) + errors.push(...result.errors) + } + return { + errors + } +} diff --git a/src/components/cylc/gscan/GScan.vue b/src/components/cylc/gscan/GScan.vue index a1c2147b4..463e06b2b 100644 --- a/src/components/cylc/gscan/GScan.vue +++ b/src/components/cylc/gscan/GScan.vue @@ -142,42 +142,14 @@ along with this program. If not, see . - - - - - - - {{ countTasksInState(scope.node.node, state) }} {{ state }}. Recent {{ state }} tasks: -
- - {{ task }}
-
-
-
-
+
@@ -203,19 +175,19 @@ import subscriptionComponentMixin from '@/mixins/subscriptionComponent' import TaskState from '@/model/TaskState.model' import SubscriptionQuery from '@/model/SubscriptionQuery.model' import { WorkflowState } from '@/model/WorkflowState.model' -import Job from '@/components/cylc/Job' import Tree from '@/components/cylc/tree/Tree' import WorkflowIcon from '@/components/cylc/gscan/WorkflowIcon' -import { addNodeToTree, createWorkflowNode } from '@/components/cylc/gscan/nodes' +import WorkflowStateSummary from '@/components/cylc/gscan/WorkflowStateSummary' +// import { addNodeToTree, createWorkflowNode } from '@/components/cylc/gscan/nodes' import { filterHierarchically } from '@/components/cylc/gscan/filters' import { GSCAN_DELTAS_SUBSCRIPTION } from '@/graphql/queries' export default { name: 'GScan', components: { - Job, Tree, - WorkflowIcon + WorkflowIcon, + WorkflowStateSummary }, mixins: [ subscriptionComponentMixin @@ -226,7 +198,9 @@ export default { GSCAN_DELTAS_SUBSCRIPTION, {}, 'root', - ['workflows/applyWorkflowsDeltas'], + [ + 'workflows/applyGScanDeltas' + ], ['workflows/clearWorkflows'] ), maximumTasksDisplayed: 5, @@ -310,16 +284,16 @@ export default { } }, computed: { - ...mapState('workflows', ['workflows']), - workflowNodes () { - // NOTE: In case we decide to allow the user to switch between hierarchical and flat - // gscan view, then all we need to do is just pass a boolean data-property to - // the `createWorkflowNode` function below. Then reactivity will take care of - // the rest. - const reducer = (acc, workflow) => addNodeToTree(createWorkflowNode(workflow, /* hierarchy */true), acc) - return Object.values(this.workflows) - .reduce(reducer, []) - }, + ...mapState('workflows', ['gscan']), + // workflowNodes () { + // // NOTE: In case we decide to allow the user to switch between hierarchical and flat + // // gscan view, then all we need to do is just pass a boolean data-property to + // // the `createWorkflowNode` function below. Then reactivity will take care of + // // the rest. + // const reducer = (acc, workflow) => addNodeToTree(createWorkflowNode(workflow, /* hierarchy */true), acc) + // return Object.values(this.workflows) + // .reduce(reducer, []) + // }, /** * @return {Array} */ @@ -348,7 +322,7 @@ export default { deep: true, immediate: false, handler: function (newVal) { - this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, this.searchWorkflows, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, this.searchWorkflows, this.workflowStates, this.taskStates) } }, /** @@ -358,13 +332,14 @@ export default { searchWorkflows: { immediate: false, handler: function (newVal) { - this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, newVal, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, newVal, this.workflowStates, this.taskStates) } }, - workflowNodes: { + gscan: { immediate: true, + deep: true, handler: function () { - this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, this.searchWorkflows, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, this.searchWorkflows, this.workflowStates, this.taskStates) } } }, @@ -412,42 +387,6 @@ export default { return `/workflows/${ node.node.name }` } return '' - }, - - /** - * Get number of tasks we have in a given state. The states are retrieved - * from `latestStateTasks`, and the number of tasks in each state is from - * the `stateTotals`. (`latestStateTasks` includes old tasks). - * - * @param {WorkflowGraphQLData} workflow - the workflow object retrieved from GraphQL - * @param {string} state - a workflow state - * @returns {number|*} - the number of tasks in the given state - */ - countTasksInState (workflow, state) { - if (Object.hasOwnProperty.call(workflow.stateTotals, state)) { - return workflow.stateTotals[state] - } - return 0 - }, - - getTaskStateClasses (workflow, state) { - const tasksInState = this.countTasksInState(workflow, state) - return tasksInState === 0 ? ['empty-state'] : [] - }, - - // TODO: temporary filter, remove after b0 - https://github.com/cylc/cylc-ui/pull/617#issuecomment-805343847 - getLatestStateTasks (latestStateTasks) { - // Values found in: https://github.com/cylc/cylc-flow/blob/9c542f9f3082d3c3d9839cf4330c41cfb2738ba1/cylc/flow/data_store_mgr.py#L143-L149 - const validValues = [ - TaskState.SUBMITTED.name, - TaskState.SUBMIT_FAILED.name, - TaskState.RUNNING.name, - TaskState.SUCCEEDED.name, - TaskState.FAILED.name - ] - return latestStateTasks.filter(entry => { - return validValues.includes(entry[0]) - }) } } } diff --git a/src/components/cylc/gscan/WorkflowStateSummary.vue b/src/components/cylc/gscan/WorkflowStateSummary.vue new file mode 100644 index 000000000..4d641d674 --- /dev/null +++ b/src/components/cylc/gscan/WorkflowStateSummary.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/cylc/gscan/deltas.js b/src/components/cylc/gscan/deltas.js index 671234a0f..20fc3051a 100644 --- a/src/components/cylc/gscan/deltas.js +++ b/src/components/cylc/gscan/deltas.js @@ -14,21 +14,150 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import Vue from 'vue' -import { mergeWith } from 'lodash' -import { mergeWithCustomizer } from '@/components/cylc/common/merge' +import * as GScanTree from '@/components/cylc/gscan/index' -export default function (data, workflows) { - const added = data.deltas.added - const updated = data.deltas.updated - const pruned = data.deltas.pruned - if (added && added.workflow && added.workflow.status) { - Vue.set(workflows, added.workflow.id, added.workflow) - } - if (updated && updated.workflow && workflows[updated.workflow.id]) { - mergeWith(workflows[updated.workflow.id], updated.workflow, mergeWithCustomizer) - } - if (pruned && pruned.workflow) { - Vue.delete(workflows, pruned.workflow) +/** + * Deltas added. + * + * @param {DeltasAdded} added + * @param {import('./index').GScan} gscan + * @param {*} options + * @returns {Result} + */ +function applyDeltasAdded (added, gscan, options) { + const result = { + errors: [] + } + if (added.workflow) { + const workflow = added.workflow + try { + GScanTree.addWorkflow(workflow, gscan, options) + } catch (error) { + result.errors.push([ + 'Error applying added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', + error, + added.workflow, + gscan, + options + ]) + } + } + return result +} + +/** + * Deltas updated. + * + * @param {DeltasUpdated} updated + * @param {import('./index').GScan} gscan + * @param {*} options + * @returns {Result} + */ +function applyDeltasUpdated (updated, gscan, options) { + const result = { + errors: [] + } + if (updated.workflow) { + const workflow = updated.workflow + try { + GScanTree.updateWorkflow(workflow, gscan, options) + } catch (error) { + result.errors.push([ + 'Error applying updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', + error, + workflow, + gscan, + options + ]) + } + } + return result +} + +/** + * Deltas pruned. + * + * @param {DeltasPruned} pruned + * @param {import('./index').GScan} gscan + * @param {*} options + * @returns {Result} + */ +function applyDeltasPruned (pruned, gscan, options) { + const result = { + errors: [] + } + // TBD: why not workflows? We have that in the jsdoc for DeltasPruned, and I believe + // that's how other queries return the data. + if (pruned.workflow) { + const workflowId = pruned.workflow + try { + GScanTree.removeWorkflow(workflowId, gscan, options) + } catch (error) { + result.errors.push([ + 'Error applying pruned-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', + error, + workflowId, + gscan, + options + ]) + } + } + return result +} + +const DELTAS = { + added: applyDeltasAdded, + updated: applyDeltasUpdated, + pruned: applyDeltasPruned +} + +/** + * Handle the deltas. This function receives the new set of deltas, and the GScan object. + * + * The GScan object contains a tree property that holds the hierarchical (or not) GScan, + * and a lookup helper dictionary used for ease of access to leaf or intermediary tree + * nodes. + * + * @param {Deltas} deltas + * @param {GScan} gscan + * @param {*} options + * @returns {Result} + */ +function handleDeltas (deltas, gscan, options) { + const results = { + errors: [] + } + Object.keys(DELTAS).forEach(key => { + if (deltas[key]) { + const handlingFunction = DELTAS[key] + const result = handlingFunction(deltas[key], gscan, options) + results.errors.push(...result.errors) + } + }) + return results +} + +/** + * @param {GraphQLResponseData} data + * @param {*} gscan + * @param {*} options + * @returns {Result} + */ +export default function (data, gscan, options) { + const deltas = data.deltas + try { + return handleDeltas(deltas, gscan, options) + } catch (error) { + return { + errors: [ + [ + 'Unexpected error applying gscan deltas', + error, + deltas, + gscan, + options + ] + ] + } } } diff --git a/src/components/cylc/gscan/index.js b/src/components/cylc/gscan/index.js new file mode 100644 index 000000000..39567e752 --- /dev/null +++ b/src/components/cylc/gscan/index.js @@ -0,0 +1,321 @@ +/** + * Copyright (C) NIWA & British Crown (Met Office) & Contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import Vue from 'vue' +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, + getWorkflowNamePartsNodesIds, + parseWorkflowNameParts +} from '@/components/cylc/gscan/nodes' + +/** + * @typedef {Object} GScan + * @property {Array} tree + * @property {Lookup} lookup + */ + +/** + * @typedef {Object} Lookup + */ + +// --- Added + +/** + * Add a new workflow to the GScan data structure. + * + * @param {TreeNode} workflow + * @param {GScan} gscan + * @param {*} options + */ +function addWorkflow (workflow, gscan, options) { + const hierarchical = options.hierarchical || true + const workflowNode = createWorkflowNode(workflow, hierarchical) + if (hierarchical) { + // TBD: We need the leaf node to propagate states, and this is done here since the + // addHierarchicalWorkflow has recursion. There might be a better way for + // handling this though? + let leafNode = workflowNode + while (leafNode.children) { + // [0] because this is not really a sparse tree, but more like a linked-list since + // we created the node with createWorkflowNode. + leafNode = leafNode.children[0] + } + addHierarchicalWorkflow(workflowNode, leafNode, gscan.lookup, gscan.tree, options) + } else { + gscan.lookup[workflow.id] = workflowNode + gscan.tree.push(workflowNode) + } +} + +/** + * This function is private. It receives a lookup and tree instead of a GScan object (as in other + * functions of this module). This is required as we apply recursion for adding nodes into the tree, + * but we replace the tree and pass only a sub-tree. + * + * @param workflowOrPart + * @param workflow + * @param {Lookup} lookup + * @param {Array} tree + * @param {*} options + * @private + */ +function addHierarchicalWorkflow (workflowOrPart, workflow, lookup, tree, options) { + if (!lookup[workflowOrPart.id]) { + // A new node. Let's add this node and its descendants to the lookup. + lookup[workflowOrPart.id] = workflowOrPart + if (workflowOrPart.children) { + const stack = [...workflowOrPart.children] + while (stack.length) { + const currentNode = stack.shift() + lookup[currentNode.id] = currentNode + if (currentNode.children) { + stack.push(...currentNode.children) + } + } + } + // And now add the node to the tree. 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 node, + // something like `sortedIndex = (array.length - sortedIndex)`. + const sortedIndex = sortedIndexBy( + tree, + workflowOrPart, + (n) => n.name, + sortWorkflowNamePartNodeOrWorkflowNode + ) + tree.splice(sortedIndex, 0, workflowOrPart) + } else { + // The node exists in the lookup, so must exist in the tree too. We will have to merge the hierarchies. + const existingNode = lookup[workflowOrPart.id] + if (existingNode.children) { + // Propagate workflow states to its ancestor. + if (workflow.node.latestStateTasks && workflow.node.stateTotals) { + existingNode.node.descendantsLatestStateTasks[workflow.id] = workflow.node.latestStateTasks + existingNode.node.descendantsStateTotal[workflow.id] = workflow.node.stateTotals + tallyPropagatedStates(existingNode.node) + } + // Copy array since we will iterate it, and modify existingNode.children + // (see the tree.splice above.) + const children = [...workflowOrPart.children] + for (const child of children) { + // Recursion! + addHierarchicalWorkflow(child, workflow, lookup, existingNode.children, options) + } + } else { + // Here we have an existing workflow node (only child-less). Let's merge it. + // It should not happen actually, since this is adding a workflow. Maybe throw + // an error instead? + mergeWith(existingNode, workflowOrPart, mergeWithCustomizer) + } + } +} + +// --- Updated + +/** + * Update a workflow in the GScan data structure. + * + * @param {WorkflowGraphQLData} workflow + * @param {GScan} gscan + * @param {Object} options + */ +function updateWorkflow (workflow, gscan, options) { + // We don't care whether it is hierarchical or not here, since we can quickly + // access the node via the GScan lookup. + const existingData = gscan.lookup[workflow.id] + if (!existingData) { + throw new Error(`Updated node [${workflow.id}] not found in workflow lookup`) + } + mergeWith(existingData.node, workflow, mergeWithCustomizer) + const hierarchical = options.hierarchical || true + if (hierarchical) { + // But now we need to propagate the states up to its ancestors, if any. + updateHierarchicalWorkflow(existingData, gscan.lookup, gscan.tree, options) + } + Vue.set(gscan.lookup, existingData.id, existingData) +} + +function updateHierarchicalWorkflow (existingData, lookup, tree, options) { + const workflowNameParts = parseWorkflowNameParts(existingData.id) + // nodesIds contains the list of GScan tree nodes, with the workflow being a leaf node. + const nodesIds = getWorkflowNamePartsNodesIds(workflowNameParts) + const workflowId = nodesIds.pop() + const parentId = nodesIds.length > 0 ? nodesIds.pop() : null + const parent = parentId ? lookup[parentId] : tree + if (!parent) { + // This is only possible if the parent was missing from the lookup... Never supposed to happen. + throw new Error(`Invalid orphan hierarchical node: ${existingData.id}`) + } + const siblings = parent.children + // Where is this node at the moment? + const currentIndex = siblings.findIndex(node => node.id === existingData.id) + // Where should it be now? + const sortedIndex = sortedIndexBy( + siblings, + existingData, + (n) => n.name, + sortWorkflowNamePartNodeOrWorkflowNode + ) + // If it is not where it must be, we need to move it to its correct location. + if (currentIndex !== sortedIndex) { + // First we remove the element from where it was. + siblings.splice(currentIndex, 1) + if (sortedIndex < currentIndex) { + // Now, if we must move the element to a position that is less than where it was, we can simply move it; + siblings.splice(sortedIndex, 0, existingData) + } else { + // however, if we are moving it down/later, we must compensate for itself. i.e. the sortedIndex is considering + // the element itself. So we subtract one from its position. + siblings.splice(sortedIndex - 1, 0, existingData) + } + } + // Finally, we need to propagate the state totals and latest state tasks, + // but only if we have a parent (otherwise we are at the top-most level). + if (parentId) { + const workflow = lookup[workflowId] + const latestStateTasks = workflow.node.latestStateTasks + const stateTotals = workflow.node.stateTotals + // Installed workflows do not have any state. + if (latestStateTasks && stateTotals) { + for (const parentNodeId of [...nodesIds, parentId]) { + const parentNode = lookup[parentNodeId] + if (parentNode.latestStateTasks && parentNode.stateTotals) { + mergeWith(parentNode.node.descendantsLatestStateTasks[workflow.id], latestStateTasks, mergeWithCustomizer) + mergeWith(parentNode.node.descendantsStateTotal[workflow.id], stateTotals, mergeWithCustomizer) + tallyPropagatedStates(parentNode.node) + } + } + } + } +} + +/** + * Computes the latestStateTasks of each node. The latestStateTasks and + * stateTotals of a workflow-name-part are not reactive, but are calculated + * based on the values of descendantsLatestStateTasks and descendantsStateTotal, + * so we need to keep these in sync any time we add or update descendants. + * + * @param {WorkflowGraphQLData} node + */ +function tallyPropagatedStates (node) { + for (const latestStateTasks of Object.values(node.descendantsLatestStateTasks)) { + for (const state of Object.keys(latestStateTasks)) { + if (node.latestStateTasks[state]) { + node.latestStateTasks[state].push(...latestStateTasks[state]) + } else { + Vue.set(node.latestStateTasks, state, latestStateTasks[state]) + } + } + } + for (const stateTotals of Object.values(node.descendantsStateTotal)) { + for (const state of Object.keys(stateTotals)) { + if (node.stateTotals[state]) { + Vue.set(node.stateTotals, state, node.stateTotals[state] + stateTotals[state]) + } else { + Vue.set(node.stateTotals, state, stateTotals[state]) + } + } + } +} + +// -- Pruned + +/** + * 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 (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} 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. + const removedNodeIds = [] + for (let i = nodesIds.length - 1; i >= 0; i--) { + const nodeId = nodesIds[i] + const node = lookup[nodeId] + // If we have children nodes, we MUST not remove the node from the GScan tree, since + // it contains other workflows branches. Instead, we must only remove the propagated + // states. + if (node.children && node.children.length > 0) { + // If we pruned a workflow that was installed, these states are undefined! + if (node.node.descendantsLatestStateTasks && node.node.descendantsStateTotal) { + for (const removedNodeId of removedNodeIds) { + Vue.delete(node.node.descendantsLatestStateTasks, removedNodeId) + Vue.delete(node.node.descendantsStateTotal, removedNodeId) + } + } + } else { + // 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) + removedNodeIds.push(nodeId) + } + } +} + +/** + * @param {String} id - ID of the tree node + * @param {Array} 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 { + addWorkflow, + updateWorkflow, + removeWorkflow +} diff --git a/src/components/cylc/gscan/nodes.js b/src/components/cylc/gscan/nodes.js index 524257d42..d388aff01 100644 --- a/src/components/cylc/gscan/nodes.js +++ b/src/components/cylc/gscan/nodes.js @@ -15,33 +15,82 @@ * along with this program. If not, see . */ -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} WorkflowGScanNode + * @typedef {Object} TreeNode * @property {String} id - * @property {String} name + * @property {String|null} name * @property {String} type * @property {WorkflowGraphQLData} node */ /** - * @typedef {Object} WorkflowNamePartGScanNode - * @property {String} id - * @property {String} name - * @property {String} type - * @property {WorkflowGraphQLData} node + * @typedef {TreeNode} WorkflowGScanNode + */ + +/** + * @typedef {TreeNode} WorkflowNamePartGScanNode * @property {Array} 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} + */ +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, workflow.id, workflow.latestStateTasks, workflow.stateTotals) + 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} + * @private */ function newWorkflowNode (workflow, part) { return { @@ -57,107 +106,104 @@ function newWorkflowNode (workflow, part) { * * @param {string} id * @param {string} part + * @param {string} workflowId + * @param {Object} latestStateTasks + * @param {Object} stateTotals * @return {WorkflowNamePartGScanNode} */ -function newWorkflowPartNode (id, part) { +function newWorkflowPartNode (id, part, workflowId, latestStateTasks = [], stateTotals = []) { return { - id: `workflow-name-part-${id}`, + id, name: part, type: 'workflow-name-part', node: { - id: id, + id, + workflowId, name: part, - status: '' + status: '', + descendantsLatestStateTasks: { + [workflowId]: latestStateTasks + }, + descendantsStateTotal: { + [workflowId]: stateTotals + }, + latestStateTasks: {}, + stateTotals: {} }, 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 - * @returns {WorkflowGScanNode|WorkflowNamePartGScanNode|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} 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} + */ +function getWorkflowNamePartsNodesIds (workflowNameParts) { + let prefix = workflowNameParts.user + const nodesIds = workflowNameParts.parts + .map(part => { + prefix = `${prefix}${workflowNameParts.partsSeparator}${part}` + return prefix + }) + nodesIds.push(workflowNameParts.workflowId) + return nodesIds } /** - * 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} nodes - * @return {Array} + * @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 } diff --git a/src/components/cylc/tree/Tree.vue b/src/components/cylc/tree/Tree.vue index dedc28157..ceb3250bc 100644 --- a/src/components/cylc/tree/Tree.vue +++ b/src/components/cylc/tree/Tree.vue @@ -218,6 +218,9 @@ export default { }) }, tasksFilterStates: function () { + if (!this.activeFilters) { + return [] + } return this.activeFilters.states.map(selectedTaskState => { return selectedTaskState }) diff --git a/src/components/cylc/tree/TreeItem.vue b/src/components/cylc/tree/TreeItem.vue index df49fd1c3..60a0c423b 100644 --- a/src/components/cylc/tree/TreeItem.vue +++ b/src/components/cylc/tree/TreeItem.vue @@ -194,7 +194,6 @@ along with this program. If not, see . . v-on:tree-item-expanded="$listeners['tree-item-expanded']" v-on:tree-item-collapsed="$listeners['tree-item-collapsed']" v-on:tree-item-clicked="$listeners['tree-item-clicked']" + > @@ -249,7 +249,7 @@ export default { active: false, selected: false, isExpanded: this.initialExpanded, - leafProperties: [ + leafProperties: Object.freeze([ { title: 'platform', property: 'platform' @@ -274,7 +274,7 @@ export default { title: 'finish time', property: 'finishedTime' } - ], + ]), filtered: true } }, diff --git a/src/components/cylc/tree/deltas.js b/src/components/cylc/tree/deltas.js index 0724aa748..f8787bb04 100644 --- a/src/components/cylc/tree/deltas.js +++ b/src/components/cylc/tree/deltas.js @@ -114,7 +114,7 @@ function applyDeltasUpdated (updated, workflow, lookup, options) { } } catch (error) { result.errors.push([ - 'Error applying added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', + 'Error applying updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', error, updatedData, workflow, @@ -210,6 +210,7 @@ function handleDeltas (deltas, workflow, lookup, options) { * @param {Workflow} workflow * @param {Lookup} lookup * @param {*} options + * @return {Result} */ export default function (data, workflow, lookup, options) { const deltas = data.deltas @@ -251,11 +252,12 @@ export default function (data, workflow, lookup, options) { return { errors: [ [ - 'Unexpected error applying deltas', + 'Unexpected error applying tree deltas', error, deltas, workflow, - lookup + lookup, + options ] ] } diff --git a/src/components/cylc/tree/index.js b/src/components/cylc/tree/index.js index 0b726b77a..e1c934d8f 100644 --- a/src/components/cylc/tree/index.js +++ b/src/components/cylc/tree/index.js @@ -430,6 +430,7 @@ function removeJob (jobId, workflow, options) { parent.children.splice(parent.children.indexOf(job), 1) } } + // TODO: for later, I believe this has already been deleted above? In recursively... Vue.delete(workflow.lookup, jobId) } } diff --git a/src/components/cylc/tree/nodes.js b/src/components/cylc/tree/nodes.js index e52ea692e..063b30e0c 100644 --- a/src/components/cylc/tree/nodes.js +++ b/src/components/cylc/tree/nodes.js @@ -38,7 +38,7 @@ import Vue from 'vue' * Create a workflow node. Uses the same properties (by reference) as the given workflow, * only adding new properties such as type, children, etc. * - * @param workflow {WorkflowGraphQLData} workflow + * @param {WorkflowGraphQLData} workflow workflow * @return {WorkflowNode} */ function createWorkflowNode (workflow) { @@ -70,7 +70,7 @@ function createWorkflowNode (workflow) { * - 'a|b' results in a cycle point node ID 'a|b' * - '' results in a cycle point node ID '' * - * @param node {GraphQLData} a tree node + * @param {GraphQLData} node a tree node * @throws {Error} - if there was an error extracting the cycle point ID * @return {string} - the cycle point ID */ diff --git a/src/components/cylc/workflow/deltas.js b/src/components/cylc/workflow/deltas.js deleted file mode 100644 index be970debe..000000000 --- a/src/components/cylc/workflow/deltas.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (C) NIWA & British Crown (Met Office) & Contributors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import Vue from 'vue' -import mergeWith from 'lodash/mergeWith' -import isArray from 'lodash/isArray' -import { mergeWithCustomizer } from '@/components/cylc/common/merge' - -/** - * @typedef {Object} Result - * @property {Array} errors - */ - -/** - * @param {DeltasAdded} added - * @param {Object.} lookup - * @return {Result} - */ -function applyDeltasAdded (added, lookup) { - const result = { - errors: [] - } - Object.values(added).forEach(addedValue => { - const items = isArray(addedValue) ? addedValue : [addedValue] - items.forEach(addedData => { - // An example for a data without .id, is the empty delta with __typename: "Added". It does occur, and can cause runtime errors. - if (addedData.id) { - try { - Vue.set(lookup, addedData.id, addedData) - } catch (error) { - result.errors.push([ - 'Error applying added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', - error, - addedData, - lookup - ]) - } - } - }) - }) - return result -} - -/** - * Deltas updated. - * - * @param updated {DeltasUpdated} updated - * @param {Object.} lookup - * @return {Result} - */ -function applyDeltasUpdated (updated, lookup) { - const result = { - errors: [] - } - Object.values(updated).forEach(updatedValue => { - const items = isArray(updatedValue) ? updatedValue : [updatedValue] - items.forEach(updatedData => { - // An example for a data without .id, is the empty delta with __typename: "Updated". It does occur, and can cause runtime errors. - if (updatedData.id) { - try { - const existingNode = lookup[updatedData.id] - if (existingNode) { - mergeWith(existingNode, updatedData, mergeWithCustomizer) - } else { - // TODO: we are adding in the updated. Is that OK? Should we revisit it later perhaps? - Vue.set(lookup, updatedData.id, updatedData) - } - } catch (error) { - result.errors.push([ - 'Error applying updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', - error, - updatedData, - lookup - ]) - } - } - }) - }) - return result -} - -/** - * Deltas pruned. - * - * @param {DeltasPruned} pruned - deltas pruned - * @param {Object.} lookup - * @return {Result} - */ -function applyDeltasPruned (pruned, lookup) { - Object.values(pruned).forEach(prunedData => { - // It can be a String, when you get a delta with only '{ __typename: "Pruned" }' - const items = isArray(prunedData) ? prunedData : [prunedData] - for (const id of items) { - if (lookup[id]) { - delete lookup[id] - } - } - }) - return { - errors: [] - } -} - -/** - * @param {*} data - * @param {Object.} lookup - */ -export default function (data, lookup) { - const added = data.deltas.added - const updated = data.deltas.updated - const pruned = data.deltas.pruned - const errors = [] - if (added) { - const result = applyDeltasAdded(added, lookup) - errors.push(...result.errors) - } - if (updated) { - const result = applyDeltasUpdated(updated, lookup) - errors.push(...result.errors) - } - if (pruned) { - const result = applyDeltasPruned(pruned, lookup) - errors.push(...result.errors) - } - return { - errors - } -} diff --git a/src/mixins/graphql.js b/src/mixins/graphql.js index 6b50f567a..38e6ae601 100644 --- a/src/mixins/graphql.js +++ b/src/mixins/graphql.js @@ -56,7 +56,7 @@ export default { /** * GraphQL query variables. * - * @returns {{workflowId: string}} + * @returns {Object.} */ variables () { return { diff --git a/src/mixins/index.js b/src/mixins/index.js index 85add48d2..7048be7b8 100644 --- a/src/mixins/index.js +++ b/src/mixins/index.js @@ -19,7 +19,6 @@ import i18n from '@/i18n' /** * Here we can define the operations that are common to components/views. - * @type {{methods: {setPageTitle(*=, *=): string}}} */ export default { /** diff --git a/src/services/mock/json/GscanSubscriptionQuery.json b/src/services/mock/json/GscanSubscriptionQuery.json index 208b78f10..b2a6e92a9 100644 --- a/src/services/mock/json/GscanSubscriptionQuery.json +++ b/src/services/mock/json/GscanSubscriptionQuery.json @@ -1,6 +1,6 @@ { "workflow": { - "id": "user|one", + "id": "user|a/b/c/one", "name": "one", "status": "running", "owner": "user", diff --git a/src/store/options.js b/src/store/options.js index b8346b556..c5ae3985f 100644 --- a/src/store/options.js +++ b/src/store/options.js @@ -19,6 +19,7 @@ import { app } from './app.module' import { workflows } from './workflows.module' import { user } from './user.module' +import { tree } from './tree.module' // State const state = { @@ -80,7 +81,8 @@ export default { modules: { app, workflows, - user + user, + tree }, actions, mutations, diff --git a/src/store/tree.module.js b/src/store/tree.module.js new file mode 100644 index 000000000..9ae9bd8d6 --- /dev/null +++ b/src/store/tree.module.js @@ -0,0 +1,118 @@ +/** + * Copyright (C) NIWA & British Crown (Met Office) & Contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import { clear } from '@/components/cylc/tree' +import applyDeltasLookup from '@/components/cylc/common/deltas' +import Alert from '@/model/Alert.model' +import applyDeltasTree from '@/components/cylc/tree/deltas' + +const state = { + /** + * This stores workflow data as a hashmap/dictionary. The keys + * are the ID's of the entities returned from GraphQL. + * + * The values of the dictionary hold the GraphQL data returned as-is. + * + * The intention is for workflow views to look up data in this structure + * and re-use, instead of duplicating it. + * + * @type {Object.} + */ + lookup: {}, + /** + * This is the CylcTree, which contains the hierarchical tree data structure. + * It is created from the GraphQL data, with the only difference that this one + * contains hierarchy, while the lookup (not workflow.lookup) is flat-ish. + * + * The nodes in the .tree property have a reference or pointer (.node) to the + * data in the lookup map above, to avoid data duplication. + * + * @type {Workflow} + */ + workflow: { + tree: {}, + lookup: {} + } +} + +const mutations = { + SET_WORKFLOW (state, data) { + state.workflow = data + }, + SET_LOOKUP (state, data) { + state.lookup = data + }, + CLEAR_WORKFLOW (state) { + clear(state.workflow) + state.workflow = { + tree: { + id: '', + type: 'workflow', + children: [] + }, + lookup: {} + } + } +} + +const actions = { + applyWorkflowDeltas ({ commit, state }, data) { + // modifying state directly in an action results in warnings... + const lookup = Object.assign({}, state.lookup) + const result = applyDeltasLookup(data, lookup) + if (result.errors.length === 0) { + commit('SET_LOOKUP', lookup) + } + result.errors.forEach(error => { + commit('SET_ALERT', new Alert(error[0], null, 'error'), { root: true }) + // eslint-disable-next-line no-console + console.warn(...error) + }) + }, + clearWorkflow ({ commit }) { + commit('SET_LOOKUP', {}) + }, + applyTreeDeltas ({ commit, state }, data) { + // modifying state directly in an action results in warnings... + const workflow = state.workflow + const lookup = state.lookup + // TODO: this could be an options object stored in the Vuex store, in some module... + const options = { + cyclePointsOrderDesc: localStorage.cyclePointsOrderDesc + ? JSON.parse(localStorage.cyclePointsOrderDesc) + : true + } + const result = applyDeltasTree(data, workflow, lookup, options) + if (result.errors.length === 0) { + commit('SET_WORKFLOW', workflow) + } + result.errors.forEach(error => { + commit('SET_ALERT', new Alert(error[0], null, 'error'), { root: true }) + // eslint-disable-next-line no-console + console.warn(...error) + }) + }, + clearTree ({ commit }) { + commit('CLEAR_WORKFLOW') + } +} + +export const tree = { + namespaced: true, + state, + mutations, + actions +} diff --git a/src/store/workflows.module.js b/src/store/workflows.module.js index d2799252a..837be3af7 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -14,46 +14,27 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import applyDeltasWorkflows from '@/components/cylc/gscan/deltas' -import applyDeltasLookup from '@/components/cylc/workflow/deltas' -import applyDeltasTree from '@/components/cylc/tree/deltas' +import Vue from 'vue' import Alert from '@/model/Alert.model' -import { clear } from '@/components/cylc/tree/index' +import applyDeltasLookup from '@/components/cylc/common/deltas' +import applyDeltasGscan from '@/components/cylc/gscan/deltas' const state = { /** - * This stores workflow data as a hashmap/dictionary. The keys - * are the ID's of the entities returned from GraphQL. - * - * The values of the dictionary hold the GraphQL data returned as-is. - * - * The intention is for workflow views to look up data in this structure - * and re-use, instead of duplicating it. + * This contains workflows returned from GraphQL indexed by their ID's. And is used by components + * such as GScan, Dashboard, and WorkflowsTable. * - * @type {Object.} + * @type {Object.} */ - lookup: {}, + workflows: {}, /** - * This is the CylcTree, which contains the hierarchical tree data structure. - * It is created from the GraphQL data, with the only difference that this one - * contains hierarchy, while the lookup (not workflow.lookup) is flat-ish. - * - * The nodes in the .tree property have a reference or pointer (.node) to the - * data in the lookup map above, to avoid data duplication. - * - * @type {Workflow} + * This is the data structure used by GScan component. The tree holds the hierarchical GScan, + * and the lookup is a helper structure for quick access to nodes in the tree. */ - workflow: { - tree: {}, + gscan: { + tree: [], lookup: {} }, - /** - * This contains a list of workflows returned from GraphQL and is used by components - * such as GScan, Dashboard, and WorkflowsTable. - * - * @type {Array} - */ - workflows: [], /** * This holds the name of the current workflow. This is set by VueRouter * and is used to decide what's the current workflow. It is used in conjunction @@ -80,25 +61,22 @@ const mutations = { SET_WORKFLOWS (state, data) { state.workflows = data }, - SET_WORKFLOW_NAME (state, data) { - state.workflowName = data + SET_GSCAN (state, data) { + state.gscan = data }, - SET_WORKFLOW (state, data) { - state.workflow = data - }, - SET_LOOKUP (state, data) { - state.lookup = data - }, - CLEAR_WORKFLOW (state) { - clear(state.workflow) - state.workflow = { - tree: { - id: '', - type: 'workflow', - children: [] - }, + CLEAR_GSCAN (state) { + for (const property of ['tree', 'lookup']) { + Object.keys(state.gscan[property]).forEach(key => { + Vue.delete(state.gscan[property], key) + }) + } + state.gscan = { + tree: [], lookup: {} } + }, + SET_WORKFLOW_NAME (state, data) { + state.workflowName = data } } @@ -109,41 +87,20 @@ const actions = { applyWorkflowsDeltas ({ commit, state }, data) { // modifying state directly in an action results in warnings... const workflows = Object.assign({}, state.workflows) - applyDeltasWorkflows(data, workflows) + applyDeltasLookup(data, workflows) commit('SET_WORKFLOWS', workflows) }, clearWorkflows ({ commit }) { commit('SET_WORKFLOWS', []) }, - applyWorkflowDeltas ({ commit, state }, data) { - // modifying state directly in an action results in warnings... - const lookup = Object.assign({}, state.lookup) - const result = applyDeltasLookup(data, lookup) - if (result.errors.length === 0) { - commit('SET_LOOKUP', lookup) - } - result.errors.forEach(error => { - commit('SET_ALERT', new Alert(error[0], null, 'error'), { root: true }) - // eslint-disable-next-line no-console - console.warn(...error) - }) - }, - clearWorkflow ({ commit }) { - commit('SET_LOOKUP', {}) - }, - applyTreeDeltas ({ commit, state }, data) { - // modifying state directly in an action results in warnings... - const workflow = state.workflow - const lookup = state.lookup - // TODO: this could be an options object stored in the Vuex store, in some module... + applyGScanDeltas ({ commit, state }, data) { + const gscan = state.gscan const options = { - cyclePointsOrderDesc: localStorage.cyclePointsOrderDesc - ? JSON.parse(localStorage.cyclePointsOrderDesc) - : true + hierarchical: true } - const result = applyDeltasTree(data, workflow, lookup, options) + const result = applyDeltasGscan(data, gscan, options) if (result.errors.length === 0) { - commit('SET_WORKFLOW', workflow) + commit('SET_GSCAN', gscan) } result.errors.forEach(error => { commit('SET_ALERT', new Alert(error[0], null, 'error'), { root: true }) @@ -151,8 +108,8 @@ const actions = { console.warn(...error) }) }, - clearTree ({ commit }) { - commit('CLEAR_WORKFLOW') + clearGScan ({ commit }) { + commit('CLEAR_GSCAN') } } diff --git a/src/views/Tree.vue b/src/views/Tree.vue index fb06f45ef..b115dab6b 100644 --- a/src/views/Tree.vue +++ b/src/views/Tree.vue @@ -70,7 +70,7 @@ export default { } }, computed: { - ...mapState('workflows', ['workflow']), + ...mapState('tree', ['workflow']), workflows () { return this.workflow && this.workflow.tree && @@ -84,12 +84,12 @@ export default { this.variables, 'workflow', [ - 'workflows/applyWorkflowDeltas', - 'workflows/applyTreeDeltas' + 'tree/applyWorkflowDeltas', + 'tree/applyTreeDeltas' ], [ - 'workflows/clearWorkflow', - 'workflows/clearTree' + 'tree/clearWorkflow', + 'tree/clearTree' ] ) }