diff --git a/CHANGES.md b/CHANGES.md
index 79ccfc057..3805ec33d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,7 +10,7 @@ release.
### Enhancements
-[#656](https://github.com/cylc/cylc-ui/pull/656) - Show runhead-limited tasks.
+[#656](https://github.com/cylc/cylc-ui/pull/656) - Show runahead-limited tasks.
[#657](https://github.com/cylc/cylc-ui/pull/657) - Display a different icon, with
a shadow underneath, for the Job component.
@@ -18,6 +18,8 @@ a shadow underneath, for the Job component.
[#658](https://github.com/cylc/cylc-ui/pull/658) - Allow user to set the order
of cycle points.
+[#543](https://github.com/cylc/cylc-ui/pull/543) - Use deltas in GScan.
+
### Fixes
[#691](https://github.com/cylc/cylc-ui/pull/691) -
diff --git a/src/components/core/Alert.vue b/src/components/core/Alert.vue
index 6c4e89199..01d436673 100644
--- a/src/components/core/Alert.vue
+++ b/src/components/core/Alert.vue
@@ -27,15 +27,17 @@ along with this program. If not, see .
light
colored-border
>
+
+ {{ svgPaths.close }}
+
{{ alert.getText() }}
diff --git a/src/components/cylc/common/deltas.js b/src/components/cylc/common/deltas.js
new file mode 100644
index 000000000..3634f2b3c
--- /dev/null
+++ b/src/components/cylc/common/deltas.js
@@ -0,0 +1,60 @@
+/**
+ * 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 .
+ */
+
+/**
+ * @typedef {Object} GraphQLResponseData
+ * @property {Deltas} deltas
+ */
+
+/**
+ * @typedef {Object} Deltas
+ * @property {string} id
+ * @property {boolean} shutdown
+ * @property {?DeltasAdded} added
+ * @property {?DeltasUpdated} updated
+ * @property {?DeltasPruned} pruned
+ */
+
+/**
+ * @typedef {Object} WorkflowGraphQLData
+ * @property {string} id
+ */
+
+/**
+ * @typedef {Object} DeltasAdded
+ * @property {WorkflowGraphQLData} workflow
+ * @property {Array} cyclePoints
+ * @property {Array} familyProxies
+ * @property {Array} taskProxies
+ * @property {Array} jobs
+ */
+
+/**
+ * @typedef {Object} DeltasUpdated
+ * @property {Object} workflow
+ * @property {Array} familyProxies
+ * @property {Array} taskProxies
+ * @property {Array} jobs
+ */
+
+/**
+ * @typedef {Object} DeltasPruned
+ * @property {Array} workflows
+ * @property {Array} taskProxies - IDs of task proxies removed
+ * @property {Array} familyProxies - IDs of family proxies removed
+ * @property {Array} jobs - IDs of jobs removed
+ */
diff --git a/src/components/cylc/common/merge.js b/src/components/cylc/common/merge.js
new file mode 100644
index 000000000..6d81f0816
--- /dev/null
+++ b/src/components/cylc/common/merge.js
@@ -0,0 +1,52 @@
+/**
+ * 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'
+
+/**
+ * Only effectively used if we return something. Otherwise Lodash will use its default merge
+ * function. We use it here not to mutate objects, but to check that we are not losing
+ * reactivity in Vue by adding a non-reactive property into an existing object (which should
+ * be reactive and used in the node tree component).
+ *
+ * @see https://docs-lodash.com/v4/merge-with/
+ * @param {?*} objValue - destination value in the existing object (same as object[key])
+ * @param {?*} srcValue - source value from the object with new values to be merged
+ * @param {string} key - name of the property being merged (used to access object[key])
+ * @param {*} object - the object being mutated (original, destination, the value is retrieved with object[key])
+ * @param {*} source - the source object
+ */
+function mergeWithCustomizer (objValue, srcValue, key, object, source) {
+ if (srcValue !== undefined) {
+ // 1. object[key], or objValue, is undefined
+ // meaning the destination object does not have the property
+ // so let's add it with reactivity!
+ if (objValue === undefined) {
+ Vue.set(object, `${key}`, srcValue)
+ }
+ // 2. object[key], or objValue, is defined but without reactivity
+ // this means somehow the object got a new property that is not reactive
+ // so let's now make it reactive with the new value!
+ if (object[key] && !object[key].__ob__) {
+ Vue.set(object, `${key}`, srcValue)
+ }
+ }
+}
+
+export {
+ mergeWithCustomizer
+}
diff --git a/src/components/cylc/gscan/GScan.vue b/src/components/cylc/gscan/GScan.vue
index d5286bdc5..2def7842a 100644
--- a/src/components/cylc/gscan/GScan.vue
+++ b/src/components/cylc/gscan/GScan.vue
@@ -14,7 +14,6 @@ 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 .
-->
-
.
.
/>
- {{ scope.node.node.name }}
+ {{ scope.node.node.name || scope.node.id }}
- {{ scope.node.node.name }}
+ {{ scope.node.node.name || scope.node.id }}
+
@@ -145,37 +144,37 @@ along with this program. If not, see .
:key="`${scope.node.id}-summary-${state}`"
:class="getTaskStateClasses(scope.node.node, state)"
>
-
-
-
-
-
-
-
-
-
-
- {{ countTasksInState(scope.node.node, state) }} {{ state }}. Recent {{ state }} tasks:
-
-
- {{ task }}
-
+
+
+
+
+
+
+
+
+
+
+ {{ countTasksInState(scope.node.node, state) }} {{ state }}. Recent {{ state }} tasks:
+
+
+ {{ task }}
-
-
+
+
+
-
+
@@ -190,18 +189,18 @@ along with this program. If not, see .
- *
- * At its first level, there is a single root node, the Workflow node.
- * Workflows have cycle points as children.
- *
- * A cycle point, in its turn, has family proxies children.
- *
- * A family proxy can have either other family proxies as children, or
- * task proxies.
- *
- * Task proxies have jobs as children.
- *
- * And jobs, finally, have job details as children.
- *
- *
- *
- * This data structure also keeps a lookup `Map` object. This map
- * contains the ID of each node as key, and the node itself as object
- * reference.
- *
- * It means that you can easily access any element in the tree, without
- * having to iterate and visit each of its parents.
- *
- *
- *
- * Finally, this data structure class contains methods to:
- *
- * - add a node of any type
- * - update a node of any type
- * - remove a node of any type
- *
- * When a node is added, it gets added to the hierarchical tree (i.e. it
- * will be added as child of some other node), and also added to the
- * lookup map.
- *
- * When a node is updated, the values of the given node argument in the
- * function replace the values in the existing element of the map. i.e.
- * we will find the object in the lookup map, and use Lodash's `merge`.
- *
- * And when a node is removed, it gets removed from its parent's `.children`
- * array, and the node and each of its children get removed from the
- * lookup map as well.
- *
- * @class
- */
-class CylcTree {
- static DEFAULT_CYCLE_POINTS_ORDER_DESC = true
- /**
- * Create a tree with an initial root node, representing
- * a workflow in Cylc.
- *
- * @param {?WorkflowNode} workflow
- * @param {*} options
- */
- constructor (workflow, options) {
- const defaults = {
- cyclePointsOrderDesc: CylcTree.DEFAULT_CYCLE_POINTS_ORDER_DESC
- }
- this.options = Object.assign(defaults, options)
- this.lookup = new Map()
- if (!workflow) {
- this.root = {
- id: '',
- node: {},
- children: []
- }
- } else {
- this.root = workflow
- this.lookup.set(this.root.id, this.root)
- }
- }
-
- /**
- * @param {WorkflowNode} workflow
- */
- setWorkflow (workflow) {
- if (!workflow) {
- throw new Error('You must provide a valid workflow!')
- }
- this.root = workflow
- this.lookup.set(workflow.id, workflow)
- }
-
- clear () {
- this.lookup.clear()
- this.root = {
- id: '',
- node: {},
- children: []
- }
- }
-
- /**
- * @returns {boolean}
- */
- isEmpty () {
- return this.lookup.size === 0
- }
-
- /**
- * @param {TreeNode} node
- */
- recursivelyRemoveNode (node) {
- const stack = [node]
- while (stack.length > 0) {
- const n = stack.pop()
- this.lookup.delete(n.id)
- if (n.children && n.children.length > 0) {
- stack.push(...n.children)
- }
- }
- }
-
- // --- Cycle points
-
- /**
- * @param {CyclePointNode} cyclePoint
- */
- addCyclePoint (cyclePoint) {
- if (!this.lookup.has(cyclePoint.id)) {
- this.lookup.set(cyclePoint.id, cyclePoint)
- const parent = this.root
- // when DESC mode, reverse to put cyclepoints in ascending order (i.e. 1, 2, 3)
- const cyclePoints = this.options.cyclePointsOrderDesc ? [...parent.children].reverse() : parent.children
- const insertIndex = sortedIndexBy(
- cyclePoints,
- cyclePoint,
- (c) => c.node.name
- )
- if (this.options.cyclePointsOrderDesc) {
- parent.children.splice(parent.children.length - insertIndex, 0, cyclePoint)
- } else {
- parent.children.splice(insertIndex, 0, cyclePoint)
- }
- }
- }
-
- /**
- * @param {CyclePointNode} cyclePoint
- */
- updateCyclePoint (cyclePoint) {
- const node = this.lookup.get(cyclePoint.id)
- if (node) {
- mergeWith(node, cyclePoint, mergeWithCustomizer)
- }
- }
-
- /**
- * @param {string} cyclePointId
- */
- removeCyclePoint (cyclePointId) {
- const node = this.lookup.get(cyclePointId)
- if (node) {
- this.recursivelyRemoveNode(node)
- this.root.children.splice(this.root.children.indexOf(node), 1)
- }
- }
-
- tallyCyclePointStates () {
- // calculate cycle point states
- computeCyclePointsStates(this.root.children)
- }
-
- // --- Family proxies
-
- /**
- * @param {FamilyProxyNode} familyProxy
- */
- addFamilyProxy (familyProxy) {
- // When we receive the families from the GraphQL endpoint, we are sorting by their
- // firstParent's. However, you may get family proxies out of order when iterating
- // them. When that happens, you may add a family proxy to the lookup, and only
- // append it to the parent later.
- // ignore the root family
- if (familyProxy.id.endsWith(`|${FAMILY_ROOT}`)) {
- return
- }
- // add if not in the lookup already
- const existingFamilyProxy = this.lookup.get(familyProxy.id)
- if (!existingFamilyProxy) {
- this.lookup.set(familyProxy.id, familyProxy)
- } else {
- // We may get a family proxy added twice. The first time is when it is the parent of another
- // family proxy. In that case, we create an orphan node in the lookup table.
- // The second time will be node with more information, such as .firstParent {}. When this happens,
- // we must remember to merge the objects.
- mergeWith(existingFamilyProxy, familyProxy, mergeWithCustomizer)
- this.lookup.set(existingFamilyProxy.id, existingFamilyProxy)
- // NOTE: important, replace the version so that we use the existing one
- // when linking with the parent node in the tree, not the new GraphQL data
- familyProxy = existingFamilyProxy
- }
- // See comment above in the else block. When we get family proxies out of order, we create the parent
- // nodes if they don't exist in the tree yet, so that we can create the correct hierarchy. Later, we
- // merge the data of the node. But for a while, the family proxy that we create won't have a state (as
- // the state is given in the deltas data, and is not available in the `node.firstParent { id }`.
- if (!familyProxy.node.state) {
- familyProxy.node.state = ''
- }
-
- // if we got the parent, let's link parent and child
- if (familyProxy.node.firstParent) {
- let parent
- if (familyProxy.node.firstParent.name === FAMILY_ROOT) {
- // if the parent is root, we use the cyclepoint as the parent
- const cyclePointId = getCyclePointId(familyProxy)
- parent = this.lookup.get(cyclePointId)
- } else if (this.lookup.has(familyProxy.node.firstParent.id)) {
- // if its parent is another family proxy node and must already exist
- parent = this.lookup.get(familyProxy.node.firstParent.id)
- } else {
- // otherwise we create it so task proxies can be added to it as a child
- parent = createFamilyProxyNode(familyProxy.node.firstParent)
- this.lookup.set(parent.id, parent)
- }
- // since this method may be called several times for the same family proxy (see comments above), it means
- // the parent-child could end up repeated by accident; it means we must make sure to create this relationship
- // exactly once.
- if (parent.children.length === 0 || !parent.children.find(child => child.id === familyProxy.id)) {
- const sortedIndex = sortedIndexBy(
- parent.children,
- familyProxy,
- (f) => f.node.name,
- sortTaskProxyOrFamilyProxy
- )
- parent.children.splice(sortedIndex, 0, familyProxy)
- }
- }
- }
-
- /**
- * @param {FamilyProxyNode} familyProxy
- */
- updateFamilyProxy (familyProxy) {
- const node = this.lookup.get(familyProxy.id)
- if (node) {
- mergeWith(node, familyProxy, mergeWithCustomizer)
- if (!node.node.state) {
- node.node.state = ''
- }
- }
- }
-
- /**
- * @param {string} familyProxyId
- */
- removeFamilyProxy (familyProxyId) {
- let node
- let nodeId
- let parentId
- // NOTE: when deleting the root family, we can also remove the entire cycle point
- if (familyProxyId.endsWith('|root')) {
- // 0 has the owner, 1 has the workflow Id, 2 has the cycle point, and 3 the family name
- const [owner, workflowId] = familyProxyId.split('|')
- nodeId = getCyclePointId({ id: familyProxyId })
- node = this.lookup.get(nodeId)
- parentId = `${owner}|${workflowId}`
- } else {
- nodeId = familyProxyId
- node = this.lookup.get(nodeId)
- if (node && node.node && node.node.firstParent) {
- if (node.node.firstParent.name === FAMILY_ROOT) {
- parentId = getCyclePointId(node)
- } else {
- parentId = node.node.firstParent.id
- }
- }
- }
- if (node) {
- this.recursivelyRemoveNode(node)
- const parent = this.lookup.get(parentId)
- // If the parent has already been removed from the lookup map, there won't be any parent here
- if (parent) {
- parent.children.splice(parent.children.indexOf(node), 1)
- }
- }
- }
-
- // --- Task proxies
-
- /**
- * Return a task proxy parent, which may be a family proxy,
- * or a cycle point (if the parent family is ROOT).
- *
- * @private
- * @param {TaskProxyNode} taskProxy
- * @return {?TaskProxyNode}
- */
- findTaskProxyParent (taskProxy) {
- if (taskProxy.node.firstParent.name === FAMILY_ROOT) {
- // if the parent is root, we must instead attach this node to the cyclepoint!
- const cyclePointId = getCyclePointId(taskProxy)
- return this.lookup.get(cyclePointId)
- }
- // otherwise its parent **MAY** already exist
- return this.lookup.get(taskProxy.node.firstParent.id)
- }
-
- /**
- * @param {TaskProxyNode} taskProxy
- */
- addTaskProxy (taskProxy) {
- if (!this.lookup.has(taskProxy.id)) {
- // progress starts at 0
- taskProxy.node.progress = 0
- // A TaskProxy could be a ghost node, which doesn't have a state/status yet.
- // Note that we cannot have this if-check in `createTaskProxyNode`, as an
- // update-delta might not have a state, and we don't want to merge
- // { state: "" } with an object that contains { state: "running" }, for
- // example.
- if (!taskProxy.node.state) {
- taskProxy.node.state = ''
- }
- this.lookup.set(taskProxy.id, taskProxy)
- if (taskProxy.node.firstParent) {
- const parent = this.findTaskProxyParent(taskProxy)
- if (!parent) {
- // eslint-disable-next-line no-console
- console.error(`Missing parent ${taskProxy.node.firstParent.id}`)
- } else {
- const sortedIndex = sortedIndexBy(
- parent.children,
- taskProxy,
- (t) => t.node.name,
- sortTaskProxyOrFamilyProxy
- )
- parent.children.splice(sortedIndex, 0, taskProxy)
- }
- }
- }
- }
-
- /**
- * @param {TaskProxyNode} taskProxy
- */
- updateTaskProxy (taskProxy) {
- const node = this.lookup.get(taskProxy.id)
- if (node) {
- mergeWith(node, taskProxy, mergeWithCustomizer)
- }
- }
-
- /**
- * @param {string} taskProxyId
- */
- removeTaskProxy (taskProxyId) {
- const taskProxy = this.lookup.get(taskProxyId)
- if (taskProxy) {
- this.recursivelyRemoveNode(taskProxy)
- // Remember that we attach task proxies children of 'root' directly to a cycle point!
- if (taskProxy.node.firstParent) {
- const parent = this.findTaskProxyParent(taskProxy)
- parent.children.splice(parent.children.indexOf(taskProxy), 1)
- }
- }
- }
-
- // --- Jobs
-
- /**
- * @param {JobNode} job
- */
- addJob (job) {
- if (!this.lookup.has(job.id)) {
- this.lookup.set(job.id, job)
- if (job.node.firstParent) {
- const parent = this.lookup.get(job.node.firstParent.id)
- const insertIndex = sortedIndexBy(
- parent.children,
- job,
- (j) => `${j.node.submitNum}`)
- parent.children.splice(parent.children.length - insertIndex, 0, job)
- }
- }
- }
-
- /**
- * @param {JobNode} job
- */
- updateJob (job) {
- const node = this.lookup.get(job.id)
- if (node) {
- mergeWith(node, job, mergeWithCustomizer)
- }
- }
-
- /**
- * @param {string} jobId
- */
- removeJob (jobId) {
- const job = this.lookup.get(jobId)
- if (job) {
- this.recursivelyRemoveNode(job)
- if (job.node.firstParent) {
- const parent = this.lookup.get(job.node.firstParent.id)
- // prevent runtime error in case the parent was already removed
- if (parent) {
- // re-calculate the job's task progress
- parent.children.splice(parent.children.indexOf(job), 1)
- }
- }
- }
- }
-}
-
-export default CylcTree
diff --git a/src/components/cylc/tree/deltas.js b/src/components/cylc/tree/deltas.js
index b953f9ce1..e51a0d64e 100644
--- a/src/components/cylc/tree/deltas.js
+++ b/src/components/cylc/tree/deltas.js
@@ -15,96 +15,62 @@
* along with this program. If not, see .
*/
+import isArray from 'lodash/isArray'
import {
+ createWorkflowNode,
createCyclePointNode,
createFamilyProxyNode,
createJobNode,
createTaskProxyNode
-} from '@/components/cylc/tree/tree-nodes'
-import { populateTreeFromGraphQLData } from '@/components/cylc/tree/index'
-import store from '@/store/index'
-import AlertModel from '@/model/Alert.model'
-
-/**
- * Helper object used to iterate pruned deltas data.
- */
-const PRUNED = {
- jobs: 'removeJob',
- taskProxies: 'removeTaskProxy',
- familyProxies: 'removeFamilyProxy'
-}
-
-/**
- * @typedef {Object} DeltasPruned
- * @property {Array} taskProxies - IDs of task proxies removed
- * @property {Array} familyProxies - IDs of family proxies removed
- * @property {Array} jobs - IDs of jobs removed
- */
-
-/**
- * Deltas pruned.
- *
- * @param {DeltasPruned} pruned - deltas pruned
- * @param {CylcTree} tree
- */
-function applyDeltasPruned (pruned, tree) {
- Object.keys(PRUNED).forEach(prunedKey => {
- if (pruned[prunedKey]) {
- for (const id of pruned[prunedKey]) {
- try {
- tree[PRUNED[prunedKey]](id)
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error('Error applying pruned-delta, will continue processing the remaining data', error, id)
- store.dispatch('setAlert', new AlertModel('Error applying pruned-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', null, 'error'))
- }
- }
- }
- })
-}
+} from '@/components/cylc/tree/nodes'
+import * as CylcTree from '@/components/cylc/tree/index'
/**
* Helper object used to iterate added deltas data.
*/
const ADDED = {
+ workflow: [createWorkflowNode, 'addWorkflow'],
cyclePoints: [createCyclePointNode, 'addCyclePoint'],
familyProxies: [createFamilyProxyNode, 'addFamilyProxy'],
taskProxies: [createTaskProxyNode, 'addTaskProxy'],
jobs: [createJobNode, 'addJob']
}
-/**
- * @typedef {Object} DeltasAdded
- * @property {Object} workflow
- * @property {Array} cyclePoints
- * @property {Array} familyProxies
- * @property {Array} taskProxies
- * @property {Array} jobs
- */
-
/**
* Deltas added.
*
* @param {DeltasAdded} added
- * @param {CylcTree} tree
+ * @param {Workflow} workflow
+ * @param {Lookup} lookup
+ * @param {*} options
*/
-function applyDeltasAdded (added, tree) {
+function applyDeltasAdded (added, workflow, lookup, options) {
+ const result = {
+ errors: []
+ }
Object.keys(ADDED).forEach(addedKey => {
if (added[addedKey]) {
- added[addedKey].forEach(addedData => {
+ const items = isArray(added[addedKey]) ? added[addedKey] : [added[addedKey]]
+ items.forEach(addedData => {
try {
+ const existingData = lookup[addedData.id]
const createNodeFunction = ADDED[addedKey][0]
const treeFunction = ADDED[addedKey][1]
- const node = createNodeFunction(addedData)
- tree[treeFunction](node)
+ const node = createNodeFunction(existingData)
+ CylcTree[treeFunction](node, workflow, options)
} catch (error) {
- // eslint-disable-next-line no-console
- console.error('Error applying added-delta, will continue processing the remaining data', error, addedData)
- store.dispatch('setAlert', new AlertModel('Error applying added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', null, '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,
+ workflow,
+ lookup
+ ])
}
})
}
})
+ return result
}
/**
@@ -116,60 +82,96 @@ const UPDATED = {
jobs: [createJobNode, 'updateJob']
}
-/**
- * @typedef {Object} DeltasUpdated
- * @property {Array} familyProxies
- * @property {Array} taskProxies
- * @property {Array} jobs
- */
-
/**
* Deltas updated.
*
* @param updated {DeltasUpdated} updated
- * @param {CylcTree} tree
+ * @param {Workflow} workflow
+ * @param {Lookup} lookup
+ * @param {*} options
*/
-function applyDeltasUpdated (updated, tree) {
+function applyDeltasUpdated (updated, workflow, lookup, options) {
+ const result = {
+ errors: []
+ }
Object.keys(UPDATED).forEach(updatedKey => {
if (updated[updatedKey]) {
updated[updatedKey].forEach(updatedData => {
try {
- const updateNodeFunction = UPDATED[updatedKey][0]
- const treeFunction = UPDATED[updatedKey][1]
- const node = updateNodeFunction(updatedData)
- tree[treeFunction](node)
+ const existingData = lookup[updatedData.id]
+ if (!existingData) {
+ result.errors.push([
+ `Updated node [${updatedData.id}] not found in workflow lookup`,
+ updatedData,
+ workflow,
+ lookup
+ ])
+ } else {
+ const updateNodeFunction = UPDATED[updatedKey][0]
+ const treeFunction = UPDATED[updatedKey][1]
+ const node = updateNodeFunction(existingData)
+ CylcTree[treeFunction](node, workflow, options)
+ }
} catch (error) {
- // eslint-disable-next-line no-console
- console.error('Error applying updated-delta, will continue processing the remaining data', error, updatedData)
- store.dispatch('setAlert', new AlertModel('Error applying updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', null, '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,
+ updatedData,
+ workflow,
+ lookup
+ ])
}
})
}
})
+ return result
}
/**
- * @typedef {Object} Deltas
- * @property {string} id
- * @property {boolean} shutdown
- * @property {?DeltasAdded} added
- * @property {?DeltasUpdated} updated
- * @property {?DeltasPruned} pruned
+ * Helper object used to iterate pruned deltas data.
*/
+const PRUNED = {
+ jobs: 'removeJob',
+ taskProxies: 'removeTaskProxy',
+ familyProxies: 'removeFamilyProxy'
+}
/**
- * Handle the initial data burst of deltas. Should create a tree given a workflow from the GraphQL
- * data. This tree contains the base structure to which the deltas are applied to.
+ * Deltas pruned.
*
- * @param {Deltas} deltas - GraphQL deltas
- * @param {CylcTree} tree - Tree object backed by an array and a Map
+ * @param {DeltasPruned} pruned - deltas pruned
+ * @param {Workflow} workflow
+ * @param {Lookup} lookup
+ * @param {*} options
*/
-function handleInitialDataBurst (deltas, tree) {
- const workflow = deltas.added.workflow
- // A workflow (e.g. five) may not have any families as 'root' is filtered
- workflow.familyProxies = workflow.familyProxies || []
- populateTreeFromGraphQLData(tree, workflow)
- tree.tallyCyclePointStates()
+function applyDeltasPruned (pruned, workflow, lookup, options) {
+ const result = {
+ errors: []
+ }
+ Object.keys(PRUNED).forEach(prunedKey => {
+ if (pruned[prunedKey]) {
+ for (const id of pruned[prunedKey]) {
+ try {
+ CylcTree[PRUNED[prunedKey]](id, workflow, 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,
+ prunedKey,
+ workflow,
+ lookup
+ ])
+ }
+ }
+ }
+ })
+ return result
+}
+
+const DELTAS = {
+ added: applyDeltasAdded,
+ updated: applyDeltasUpdated,
+ pruned: applyDeltasPruned
}
/**
@@ -181,71 +183,85 @@ function handleInitialDataBurst (deltas, tree) {
* the first family from the top of the hierarchy in the deltas.
*l
* @param {Deltas} deltas - GraphQL deltas
- * @param {CylcTree} tree - Tree object backed by an array and a Map
+ * @param {Workflow} workflow - Tree object
+ * @param {Lookup} lookup
+ * @param {*} options
*/
-function handleDeltas (deltas, tree) {
- if (deltas.pruned) {
- applyDeltasPruned(deltas.pruned, tree)
- }
- if (deltas.added) {
- applyDeltasAdded(deltas.added, tree)
- }
- if (deltas.updated) {
- applyDeltasUpdated(deltas.updated, tree)
- }
+function handleDeltas (deltas, workflow, lookup, options) {
+ const errors = []
+ Object.keys(DELTAS).forEach(key => {
+ if (deltas[key]) {
+ const handlingFunction = DELTAS[key]
+ const result = handlingFunction(deltas[key], workflow, lookup, options)
+ errors.push(...result.errors)
+ }
+ })
// if added, removed, or updated deltas, we want to re-calculate the cycle point states now
if (deltas.pruned || deltas.added || deltas.updated) {
- tree.tallyCyclePointStates()
+ CylcTree.tallyCyclePointStates(workflow)
+ }
+ return {
+ errors
}
}
/**
- * @param {?Deltas} deltas
- * @param {?CylcTree} tree
+ * @param {GraphQLResponseData} data
+ * @param {Workflow} workflow
+ * @param {Lookup} lookup
+ * @param {*} options
*/
-export function applyDeltas (deltas, tree) {
- if (deltas && tree) {
- // first we check whether it is a shutdown response
- if (deltas.shutdown) {
- tree.clear()
- return
+export default function (data, workflow, lookup, options) {
+ const deltas = data.deltas
+ // first we check whether it is a shutdown response
+ if (deltas.shutdown) {
+ CylcTree.clear(workflow)
+ return {
+ errors: []
}
- if (tree.isEmpty()) {
- // When the tree is null, we have two possible scenarios:
- // 1. This means that we will receive our initial data burst in deltas.added.workflow
- // which we can use to create the tree structure.
- // 2. Or this means that after the shutdown (when we delete the tree), we received a delta.
- // In this case we don't really have any way to fix the tree.
- // In both cases, actually, the user has little that s/he could do, besides refreshing the
- // page. So we fail silently and wait for a request with the initial data.
- if (!deltas.added || !deltas.added.workflow) {
- // eslint-disable-next-line no-console
- console.error('Received a delta before the workflow initial data burst')
- store.dispatch('setAlert', new AlertModel('Received a delta before the workflow initial data burst. Please reload your browser tab to retrieve the full flow state', null, 'error'))
- return
- }
- try {
- handleInitialDataBurst(deltas, tree)
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error('Error applying initial data burst for deltas', error, deltas)
- store.dispatch('setAlert', new AlertModel('Error applying initial data burst for deltas. Please reload your browser tab to retrieve the full flow state', null, 'error'))
- throw error
- }
- } else {
- // the tree was created, and now the next messages should contain
- // 1. new data added under deltas.added (but not in deltas.added.workflow)
- // 2. data updated in deltas.updated
- // 3. data pruned in deltas.pruned
- try {
- handleDeltas(deltas, tree)
- } catch (error) {
- // eslint-disable-next-line no-console
- console.error('Unexpected error applying deltas', error, deltas)
- throw error
+ }
+ // Safe check in case the tree is empty.
+ if (CylcTree.isEmpty(workflow)) {
+ // When the tree is empty, we have two possible scenarios:
+ // 1. This means that we will receive our initial data burst in deltas.added
+ // which we can use to create the tree structure.
+ // 2. Or this means that after the shutdown (when we delete the tree), we received a delta.
+ // In this case we don't really have any way to fix the tree.
+ // In both cases, actually, the user has little that s/he could do, besides refreshing the
+ // page. So we fail silently and wait for a request with the initial data.
+ //
+ // We need at least a deltas.added.workflow in the deltas data, since it is the root node.
+ if (!deltas.added || !deltas.added.workflow) {
+ return {
+ errors: [
+ [
+ 'Received a delta before the workflow initial data burst',
+ deltas.added,
+ workflow,
+ lookup
+ ]
+ ]
}
}
- } else {
- throw Error('Workflow tree subscription did not return data.deltas')
+ }
+ // the tree was created, and now the next messages should contain
+ // 1. data added in deltas.added
+ // 2. data updated in deltas.updated
+ // 3. data pruned in deltas.pruned
+ // 4. a delta with some data, and the .shutdown flag telling us the workflow has stopped
+ try {
+ return handleDeltas(deltas, workflow, lookup, options)
+ } catch (error) {
+ return {
+ errors: [
+ [
+ 'Unexpected error applying deltas',
+ error,
+ deltas,
+ workflow,
+ lookup
+ ]
+ ]
+ }
}
}
diff --git a/src/components/cylc/tree/index.js b/src/components/cylc/tree/index.js
index b84a4b9d2..82c193e47 100644
--- a/src/components/cylc/tree/index.js
+++ b/src/components/cylc/tree/index.js
@@ -14,58 +14,548 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+import { extractGroupState } from '@/utils/tasks'
+import { mergeWith } from 'lodash'
+import { createFamilyProxyNode, getCyclePointId } from '@/components/cylc/tree/nodes'
+import Vue from 'vue'
+import { mergeWithCustomizer } from '@/components/cylc/common/merge'
-// eslint-disable-next-line no-unused-vars
-import CylcTree from '@/components/cylc/tree/cylc-tree'
-import {
- containsTreeData,
- createWorkflowNode,
- createCyclePointNode,
- createFamilyProxyNode,
- createTaskProxyNode,
- createJobNode
-} from '@/components/cylc/tree/tree-nodes'
+export const FAMILY_ROOT = 'root'
+
+export const DEFAULT_CYCLE_POINTS_ORDER_DESC = true
+
+/**
+ * @typedef {Object} Workflow
+ * @property {Lookup} lookup
+ * @property {Tree} tree
+ */
+
+/**
+ * @typedef {Object} Lookup
+ */
+
+/**
+ * @typedef {Object} Tree
+ */
+
+/**
+ * Compute the state of each cycle point node in the list given.
+ *
+ * The formula used to compute each cycle point state is the same as in Cylc 7, using an enum of task types.
+ *
+ * After the state is successfully computed, each cycle point node gets an additional property `state`
+ * with type string, representing the cycle point state.
+ *
+ * @param {Array} cyclePointNodes list of cycle point nodes.
+ */
+function computeCyclePointsStates (cyclePointNodes) {
+ for (const cyclePointNode of cyclePointNodes) {
+ const childStates = []
+ for (const child of cyclePointNode.children) {
+ childStates.push(child.node.state)
+ }
+ const cyclePointState = extractGroupState(childStates, false)
+ // Initially the .node object retrieved from the GraphQL endpoint does
+ // not have the .state property. So we need to ask Vue to make it reactive.
+ Vue.set(cyclePointNode.node, 'state', cyclePointState)
+ }
+}
+
+/**
+ * The default comparator used to compare strings for cycle points, family proxies names,
+ * task proxies names, and jobs.
+ *
+ * @param left {string}
+ * @param right {string}
+ * @returns {number}
+ * @constructor
+ */
+const DEFAULT_COMPARATOR = (left, right) => {
+ return left.toLowerCase()
+ .localeCompare(
+ right.toLowerCase(),
+ undefined,
+ {
+ numeric: true,
+ sensitivity: 'base'
+ }
+ )
+}
+
+/**
+ * Declare function used in sortedIndexBy as a comparator.
+ *
+ * @private
+ * @callback SortedIndexByComparator
+ * @param {object} leftObject - left parameter object
+ * @param {string} leftValue - left parameter value
+ * @param {object} rightObject - right parameter object
+ * @param {string} rightValue - right parameter value
+ * @returns {boolean} - true if leftValue is higher than rightValue
+ */
+
+/**
+ * @private
+ * @typedef {SortedIndexByComparator} SortTaskProxyOrFamilyProxyComparator
+ * @param {TaskProxyNode|FamilyProxyNode} leftObject
+ * @param {string} leftValue
+ * @param {TaskProxyNode|FamilyProxyNode} rightObject
+ * @param {string} rightValue
+ * @returns {boolean}
+ */
+function sortTaskProxyOrFamilyProxy (leftObject, leftValue, rightObject, rightValue) {
+ // sort cycle point children (family-proxies, and task-proxies)
+ // first we sort by type ascending, so 'family-proxy' types come before 'task-proxy'
+ // then we sort by node name ascending, so 'bar' comes before 'foo'
+ // node type
+ if (leftObject.type < rightObject.type) {
+ return -1
+ }
+ if (leftObject.type > rightObject.type) {
+ return 1
+ }
+ // name
+ return DEFAULT_COMPARATOR(leftValue, rightValue) > 0
+}
+
+/**
+ * Declare function used in sortedIndexBy for creating the iteratee.
+ *
+ * @callback SortedIndexByIteratee
+ * @param {Object} value - any object
+ * @returns {string}
+ */
/**
- * Populate the given tree using the also provided GraphQL workflow object.
+ * Given a list of elements, and a value to be added to the list, we
+ * perform a simple binary search of the list to determine the next
+ * index where the value can be inserted, so that the list remains
+ * sorted.
*
- * Every node has data, and a .name property used to display the node in the tree in the UI.
+ * This function uses localeCompare, which will respect the numeric
+ * collation.
*
- * @param tree {null|CylcTree} - A hierarchical tree
- * @param workflow {null|Object} - GraphQL workflow object
- * @throws {Error} - If the workflow or tree are either null or invalid (e.g. missing data)
- */
-function populateTreeFromGraphQLData (tree, workflow) {
- if (!tree || !workflow || !containsTreeData(workflow)) {
- // throw new Error('You must provide valid data to populate the tree!')
- // a stopped workflow is valid, but won't have anything that we can use
- // to populate the tree, only workflow data and empty families
+ * This is a simplified version of lodash's function with the same
+ * name, but that respects natural order for numbers, i.e. [1, 2, 10].
+ * Not [1, 10, 2].
+ *
+ * @private
+ * @param array {Array} - list of string values, or of objects with string values
+ * @param value {object} - a value to be inserted in the list, or an object wrapping the value (see iteratee)
+ * @param iteratee {SortedIndexByIteratee=} - an optional function used to return the value of the element of the list}
+ * @param comparator {SortedIndexByComparator=} - function used to compare the newValue with otherValues in the list
+ */
+function sortedIndexBy (array, value, iteratee, comparator) {
+ if (array.length === 0) {
+ return 0
+ }
+ // If given a function, use it. Otherwise, simply use identity function.
+ const iterateeFunction = iteratee || ((value) => value)
+ // If given a function, use it. Otherwise, simply use locale sort with numeric enabled
+ const comparatorFunction = comparator || ((leftObject, leftValue, rightObject, rightValue) => DEFAULT_COMPARATOR(leftValue, rightValue) > 0)
+ let low = 0
+ let high = array.length
+
+ const newValue = iterateeFunction(value)
+
+ while (low < high) {
+ const mid = Math.floor((low + high) / 2)
+ const midValue = iterateeFunction(array[mid])
+ const higher = comparatorFunction(value, newValue, array[mid], midValue)
+ if (higher) {
+ low = mid + 1
+ } else {
+ high = mid
+ }
+ }
+ return high
+}
+
+/**
+ * @param {Workflow} workflow
+ */
+function clear (workflow) {
+ ['tree', 'lookup'].forEach(each => {
+ Object.keys(workflow[each]).forEach(key => {
+ Vue.delete(workflow[each], key)
+ })
+ })
+}
+
+/**
+ * @param {Workflow} workflow
+ */
+function isEmpty (workflow) {
+ return Object.keys(workflow.lookup).length === 0
+}
+
+/**
+ * @private
+ * @param {TreeNode} node
+ * @param {Lookup} lookup
+ */
+function recursivelyRemoveNode (node, lookup) {
+ const stack = [node]
+ while (stack.length > 0) {
+ const n = stack.pop()
+ Vue.delete(lookup, n.id)
+ if (n.children && n.children.length > 0) {
+ stack.push(...n.children)
+ }
+ }
+}
+
+// --- Workflow
+
+function addWorkflow (workflowNode, workflow, options) {
+ if (!workflow.lookup[workflowNode.id]) {
+ mergeWith(workflow.tree, workflowNode, mergeWithCustomizer)
+ Vue.set(workflow.lookup, workflowNode.id, workflow.tree)
+ }
+}
+
+// --- Cycle points
+
+/**
+ * @param {Workflow} workflow
+ * @param {CyclePointNode} cyclePoint
+ * @param {*} options
+ */
+function addCyclePoint (cyclePoint, workflow, options) {
+ if (!workflow.lookup[cyclePoint.id]) {
+ Vue.set(workflow.lookup, cyclePoint.id, cyclePoint)
+ const parent = workflow.tree
+ // when DESC mode, reverse to put cyclepoints in ascending order (i.e. 1, 2, 3)
+ const cyclePointsOrderDesc = options.cyclePointsOrderDesc !== undefined
+ ? options.cyclePointsOrderDesc
+ : DEFAULT_CYCLE_POINTS_ORDER_DESC
+ const cyclePoints = cyclePointsOrderDesc ? [...parent.children].reverse() : parent.children
+ const insertIndex = sortedIndexBy(
+ cyclePoints,
+ cyclePoint,
+ (c) => c.node.name
+ )
+ if (cyclePointsOrderDesc) {
+ parent.children.splice(parent.children.length - insertIndex, 0, cyclePoint)
+ } else {
+ parent.children.splice(insertIndex, 0, cyclePoint)
+ }
+ }
+}
+
+/**
+ * @param {CyclePointNode} cyclePoint
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function updateCyclePoint (cyclePoint, workflow, options) {
+ const node = workflow.lookup[cyclePoint.id]
+ if (node) {
+ mergeWith(node, cyclePoint, mergeWithCustomizer)
+ }
+}
+
+/**
+ * @param {String} cyclePointId
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function removeCyclePoint (cyclePointId, workflow, options) {
+ const node = workflow.lookup[cyclePointId]
+ if (node) {
+ recursivelyRemoveNode(node, workflow.lookup)
+ workflow.tree.children.splice(workflow.tree.children.indexOf(node), 1)
+ Vue.delete(workflow.lookup, cyclePointId)
+ }
+}
+
+/**
+ * @param {Workflow} workflow
+ */
+function tallyCyclePointStates (workflow) {
+ if (workflow && workflow.tree && workflow.tree.children) {
+ // calculate cycle point states
+ computeCyclePointsStates(workflow.tree.children)
+ }
+}
+
+// --- Family proxies
+
+/**
+ * @param {FamilyProxyNode} familyProxy
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function addFamilyProxy (familyProxy, workflow, options) {
+ // When we receive the families from the GraphQL endpoint, we are sorting by their
+ // firstParent's. However, you may get family proxies out of order when iterating
+ // them. When that happens, you may add a family proxy to the lookup, and only
+ // append it to the parent later.
+ // ignore the root family
+ if (familyProxy.id.endsWith(`|${FAMILY_ROOT}`)) {
return
}
- // the workflow object gets augmented to become a valid node for the tree
- const rootNode = createWorkflowNode(workflow)
- tree.setWorkflow(rootNode)
- for (const cyclePoint of workflow.cyclePoints) {
- const cyclePointNode = createCyclePointNode(cyclePoint)
- tree.addCyclePoint(cyclePointNode)
- }
- for (const familyProxy of workflow.familyProxies) {
- const familyProxyNode = createFamilyProxyNode(familyProxy)
- tree.addFamilyProxy(familyProxyNode)
- }
- for (const taskProxy of workflow.taskProxies) {
- const taskProxyNode = createTaskProxyNode(taskProxy)
- tree.addTaskProxy(taskProxyNode)
- // A TaskProxy could no jobs (yet)
- if (taskProxy.jobs) {
- for (const job of taskProxy.jobs) {
- const jobNode = createJobNode(job)
- tree.addJob(jobNode)
+ // add if not in the lookup already
+ const existingFamilyProxy = workflow.lookup[familyProxy.id]
+ if (!existingFamilyProxy) {
+ Vue.set(workflow.lookup, familyProxy.id, familyProxy)
+ } else {
+ // We may get a family proxy added twice. The first time is when it is the parent of another
+ // family proxy. In that case, we create an orphan node in the lookup table.
+ // The second time will be node with more information, such as .firstParent {}. When this happens,
+ // we must remember to merge the objects.
+ mergeWith(existingFamilyProxy, familyProxy, mergeWithCustomizer)
+ Vue.set(workflow.lookup, existingFamilyProxy.id, existingFamilyProxy)
+ // NOTE: important, replace the version so that we use the existing one
+ // when linking with the parent node in the tree, not the new GraphQL data
+ familyProxy = existingFamilyProxy
+ }
+ // See comment above in the else block. When we get family proxies out of order, we create the parent
+ // nodes if they don't exist in the tree yet, so that we can create the correct hierarchy. Later, we
+ // merge the data of the node. But for a while, the family proxy that we create won't have a state (as
+ // the state is given in the deltas data, and is not available in the `node.firstParent { id }`.
+ if (!familyProxy.node.state) {
+ Vue.set(familyProxy.node, 'state', '')
+ }
+
+ // if we got the parent, let's link parent and child
+ if (familyProxy.node.firstParent) {
+ let parent
+ if (familyProxy.node.firstParent.name === FAMILY_ROOT) {
+ // if the parent is root, we use the cyclepoint as the parent
+ const cyclePointId = getCyclePointId(familyProxy)
+ parent = workflow.lookup[cyclePointId]
+ } else if (workflow.lookup[familyProxy.node.firstParent.id]) {
+ // if its parent is another family proxy node and must already exist
+ parent = workflow.lookup[familyProxy.node.firstParent.id]
+ } else {
+ // otherwise we create it so task proxies can be added to it as a child
+ parent = createFamilyProxyNode(familyProxy.node.firstParent)
+ Vue.set(workflow.lookup, parent.id, parent)
+ }
+ // since this method may be called several times for the same family proxy (see comments above), it means
+ // the parent-child could end up repeated by accident; it means we must make sure to create this relationship
+ // exactly once.
+ if (parent.children.length === 0 || !parent.children.find(child => child.id === familyProxy.id)) {
+ const sortedIndex = sortedIndexBy(
+ parent.children,
+ familyProxy,
+ (f) => f.node.name,
+ sortTaskProxyOrFamilyProxy
+ )
+ parent.children.splice(sortedIndex, 0, familyProxy)
+ }
+ }
+}
+
+/**
+ * @param {FamilyProxyNode} familyProxy
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function updateFamilyProxy (familyProxy, workflow, options) {
+ const node = workflow.lookup[familyProxy.id]
+ if (node) {
+ mergeWith(node, familyProxy, mergeWithCustomizer)
+ if (!node.node.state) {
+ Vue.set(node.node, 'state', '')
+ }
+ }
+}
+
+// --- Task proxies
+
+/**
+ * Return a task proxy parent, which may be a family proxy,
+ * or a cycle point (if the parent family is ROOT).
+ *
+ * @private
+ * @param {TaskProxyNode} taskProxy
+ * @param {Workflow} workflow
+ * @return {?TaskProxyNode}
+ */
+function findTaskProxyParent (taskProxy, workflow) {
+ if (taskProxy.node.firstParent.name === FAMILY_ROOT) {
+ // if the parent is root, we must instead attach this node to the cyclepoint!
+ const cyclePointId = getCyclePointId(taskProxy)
+ return workflow.lookup[cyclePointId]
+ }
+ // otherwise its parent **MAY** already exist
+ return workflow.lookup[taskProxy.node.firstParent.id]
+}
+
+/**
+ * @param {TaskProxyNode} taskProxy
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function addTaskProxy (taskProxy, workflow, options) {
+ if (!workflow.lookup[taskProxy.id]) {
+ // progress starts at 0
+ Vue.set(taskProxy.node, 'progress', 0)
+ // A TaskProxy could be a ghost node, which doesn't have a state/status yet.
+ // Note that we cannot have this if-check in `createTaskProxyNode`, as an
+ // update-delta might not have a state, and we don't want to merge
+ // { state: "" } with an object that contains { state: "running" }, for
+ // example.
+ if (!taskProxy.node.state) {
+ Vue.set(taskProxy.node, 'state', '')
+ }
+ Vue.set(workflow.lookup, taskProxy.id, taskProxy)
+ if (taskProxy.node.firstParent) {
+ const parent = findTaskProxyParent(taskProxy, workflow)
+ if (!parent) {
+ // eslint-disable-next-line no-console
+ console.error(`Missing parent ${taskProxy.node.firstParent.id}`)
+ } else {
+ const sortedIndex = sortedIndexBy(
+ parent.children,
+ taskProxy,
+ (t) => t.node.name,
+ sortTaskProxyOrFamilyProxy
+ )
+ parent.children.splice(sortedIndex, 0, taskProxy)
+ }
+ }
+ }
+}
+
+/**
+ * @param {TaskProxyNode} taskProxy
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function updateTaskProxy (taskProxy, workflow, options) {
+ const node = workflow.lookup[taskProxy.id]
+ if (node) {
+ mergeWith(node, taskProxy, mergeWithCustomizer)
+ }
+}
+
+/**
+ * @param {string} taskProxyId
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function removeTaskProxy (taskProxyId, workflow, options) {
+ const taskProxy = workflow.lookup[taskProxyId]
+ if (taskProxy) {
+ recursivelyRemoveNode(taskProxy, workflow.lookup)
+ // Remember that we attach task proxies children of 'root' directly to a cycle point!
+ if (taskProxy.node.firstParent) {
+ const parent = findTaskProxyParent(taskProxy, workflow)
+ parent.children.splice(parent.children.indexOf(taskProxy), 1)
+ }
+ Vue.delete(workflow.lookup, taskProxyId)
+ }
+}
+
+/**
+ * @param {String} familyProxyId
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function removeFamilyProxy (familyProxyId, workflow, options) {
+ let node
+ let nodeId
+ let parentId
+ // NOTE: when deleting the root family, we can also remove the entire cycle point
+ if (familyProxyId.endsWith('|root')) {
+ // 0 has the owner, 1 has the workflow Id, 2 has the cycle point, and 3 the family name
+ const [owner, workflowId] = familyProxyId.split('|')
+ nodeId = getCyclePointId({ id: familyProxyId })
+ node = workflow.lookup[nodeId]
+ parentId = `${owner}|${workflowId}`
+ } else {
+ nodeId = familyProxyId
+ node = workflow.lookup[nodeId]
+ if (node && node.node && node.node.firstParent) {
+ if (node.node.firstParent.name === FAMILY_ROOT) {
+ parentId = getCyclePointId(node)
+ } else {
+ parentId = node.node.firstParent.id
}
}
}
+ if (node) {
+ recursivelyRemoveNode(node, workflow.lookup)
+ const parent = workflow.lookup[parentId]
+ // If the parent has already been removed from the lookup map, there won't be any parent here
+ if (parent) {
+ parent.children.splice(parent.children.indexOf(node), 1)
+ }
+ Vue.delete(workflow.lookup, node.id)
+ }
+}
+
+// --- Jobs
+
+/**
+ * @param {JobNode} job
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function addJob (job, workflow, options) {
+ if (!workflow.lookup[job.id]) {
+ Vue.set(workflow.lookup, job.id, job)
+ if (job.node.firstParent) {
+ const parent = workflow.lookup[job.node.firstParent.id]
+ const insertIndex = sortedIndexBy(
+ parent.children,
+ job,
+ (j) => `${j.node.submitNum}`)
+ parent.children.splice(parent.children.length - insertIndex, 0, job)
+ }
+ }
}
+/**
+ * @param {JobNode} job
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function updateJob (job, workflow, options) {
+ const node = workflow.lookup[job.id]
+ if (node) {
+ mergeWith(node, job, mergeWithCustomizer)
+ }
+}
+
+/**
+ * @param {string} jobId
+ * @param {Workflow} workflow
+ * @param {*} options
+ */
+function removeJob (jobId, workflow, options) {
+ const job = workflow.lookup[jobId]
+ if (job) {
+ recursivelyRemoveNode(job, workflow.lookup)
+ if (job.node.firstParent) {
+ const parent = workflow.lookup[job.node.firstParent.id]
+ // prevent runtime error in case the parent was already removed
+ if (parent) {
+ // re-calculate the job's task progress
+ parent.children.splice(parent.children.indexOf(job), 1)
+ }
+ }
+ Vue.delete(workflow.lookup, jobId)
+ }
+}
export {
- populateTreeFromGraphQLData
+ clear,
+ isEmpty,
+ addWorkflow,
+ addCyclePoint,
+ updateCyclePoint,
+ removeCyclePoint,
+ tallyCyclePointStates,
+ addFamilyProxy,
+ updateFamilyProxy,
+ removeFamilyProxy,
+ addTaskProxy,
+ updateTaskProxy,
+ removeTaskProxy,
+ addJob,
+ updateJob,
+ removeJob
}
diff --git a/src/components/cylc/tree/tree-nodes.js b/src/components/cylc/tree/nodes.js
similarity index 96%
rename from src/components/cylc/tree/tree-nodes.js
rename to src/components/cylc/tree/nodes.js
index f08233c4d..1aea5e2ca 100644
--- a/src/components/cylc/tree/tree-nodes.js
+++ b/src/components/cylc/tree/nodes.js
@@ -343,24 +343,11 @@ function createJobNode (job) {
}
}
-/**
- * @param {?WorkflowGraphQLData} workflow
- * @returns {boolean}
- */
-function containsTreeData (workflow) {
- return workflow !== undefined &&
- workflow !== null &&
- workflow.cyclePoints && Array.isArray(workflow.cyclePoints) &&
- workflow.familyProxies && Array.isArray(workflow.familyProxies) &&
- workflow.taskProxies && Array.isArray(workflow.taskProxies)
-}
-
export {
createWorkflowNode,
createCyclePointNode,
createFamilyProxyNode,
createTaskProxyNode,
createJobNode,
- containsTreeData,
getCyclePointId
}
diff --git a/src/components/cylc/workflow/Toolbar.vue b/src/components/cylc/workflow/Toolbar.vue
index 428d21868..e11725ea8 100644
--- a/src/components/cylc/workflow/Toolbar.vue
+++ b/src/components/cylc/workflow/Toolbar.vue
@@ -91,14 +91,14 @@ along with this program. If not, see .
{{ view.icon }}
- {{ view.title }}
+ {{ view.name }}
@@ -123,10 +123,9 @@ along with this program. If not, see .
diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue
index 6ea27fe07..026241b9a 100644
--- a/src/views/NotFound.vue
+++ b/src/views/NotFound.vue
@@ -46,10 +46,10 @@ along with this program. If not, see .
diff --git a/src/views/UserProfile.vue b/src/views/UserProfile.vue
index 3a81e446d..b33eb6ff3 100644
--- a/src/views/UserProfile.vue
+++ b/src/views/UserProfile.vue
@@ -193,23 +193,23 @@ along with this program. If not, see .