From 357bf240755be9519bc0b1e2882dbfe22ccc8184 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 23 Aug 2021 09:26:05 +1200 Subject: [PATCH 01/19] Fix jsdoc --- src/mixins/graphql.js | 2 +- src/store/workflows.module.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/store/workflows.module.js b/src/store/workflows.module.js index 49a4e6cbb..a2fb12290 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -44,10 +44,10 @@ const state = { lookup: {} }, /** - * This contains a list of workflows returned from GraphQL and is used by components + * 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.} */ workflows: {}, /** From 5c6b02cfe033a40215322d3124531baadeff339b Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 23 Aug 2021 10:19:21 +1200 Subject: [PATCH 02/19] Move tree workflow data into its own Vuex module to avoid confusion --- src/mixins/index.js | 1 - src/store/options.js | 4 +- src/store/tree.module.js | 118 ++++++++++++++++++++++++++++++++++ src/store/workflows.module.js | 58 ++++------------- src/views/Tree.vue | 2 +- 5 files changed, 136 insertions(+), 47 deletions(-) create mode 100644 src/store/tree.module.js 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/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..277160e80 --- /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/workflow/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 a2fb12290..fd22126c1 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -14,35 +14,9 @@ * 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/index' +import applyDeltasWorkflows 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. - * - * @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: {} - }, /** * This contains workflows returned from GraphQL indexed by their ID's. And is used by components * such as GScan, Dashboard, and WorkflowsTable. @@ -78,28 +52,24 @@ const mutations = { }, SET_WORKFLOWS (state, data) { state.workflows = data + } +} + +const actions = { + setWorkflowName ({ commit }, data) { + commit('SET_WORKFLOW_NAME', data) }, - SET_WORKFLOW (state, data) { - state.workflow = data - }, - SET_LOOKUP (state, data) { - state.lookup = data + applyWorkflowsDeltas ({ commit, state }, data) { + // modifying state directly in an action results in warnings... + const workflows = Object.assign({}, state.workflows) + applyDeltasWorkflows(data, workflows) + commit('SET_WORKFLOWS', workflows) }, - CLEAR_WORKFLOW (state) { - clear(state.workflow) - state.workflow = { - tree: { - id: '', - type: 'workflow', - children: [] - }, - lookup: {} - } + clearWorkflows ({ commit }) { + commit('SET_WORKFLOWS', []) } } -const actions = {} - export const workflows = { namespaced: true, state, diff --git a/src/views/Tree.vue b/src/views/Tree.vue index 06adb21f2..0d64638c2 100644 --- a/src/views/Tree.vue +++ b/src/views/Tree.vue @@ -72,7 +72,7 @@ export default { } }, computed: { - ...mapState('workflows', ['workflow']), + ...mapState('tree', ['workflow']), workflows () { return this.workflow && this.workflow.tree && From 3126f30c0f66e468814d0c13dd132d5959cc7b71 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 23 Aug 2021 13:29:36 +1200 Subject: [PATCH 03/19] Move the function that applies deltas to a lookup-like structure to a common module (so GScan can re-use it too) --- src/components/cylc/common/deltas.js | 43 +++++++++++++++++++++++----- src/store/tree.module.js | 2 +- src/store/workflows.module.js | 37 +++++++++++++++++++++++- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/components/cylc/common/deltas.js b/src/components/cylc/common/deltas.js index e1170f42e..5c673fe66 100644 --- a/src/components/cylc/common/deltas.js +++ b/src/components/cylc/common/deltas.js @@ -94,7 +94,7 @@ import { mergeWithCustomizer } from '@/components/cylc/common/merge' const KEYS = ['workflow', 'cyclePoints', 'familyProxies', 'taskProxies', 'jobs'] /** - * @param {DeltasAdded|Object} added + * @param {DeltasAdded} added * @param {Object.} lookup * @return {Result} */ @@ -126,7 +126,7 @@ function applyDeltasAdded (added, lookup) { /** * Deltas updated. * - * @param updated {DeltasUpdated|Object} updated + * @param updated {DeltasUpdated} updated * @param {Object.} lookup * @return {Result} */ @@ -164,7 +164,7 @@ function applyDeltasUpdated (updated, lookup) { /** * Deltas pruned. * - * @param {DeltasPruned|Object} pruned - deltas pruned + * @param {DeltasPruned} pruned - deltas pruned * @param {Object.} lookup * @return {Result} */ @@ -182,8 +182,37 @@ function applyDeltasPruned (pruned, lookup) { } } -export { - applyDeltasAdded, - applyDeltasUpdated, - applyDeltasPruned +/** + * 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/store/tree.module.js b/src/store/tree.module.js index 277160e80..9ae9bd8d6 100644 --- a/src/store/tree.module.js +++ b/src/store/tree.module.js @@ -15,7 +15,7 @@ * along with this program. If not, see . */ import { clear } from '@/components/cylc/tree' -import applyDeltasLookup from '@/components/cylc/workflow/deltas' +import applyDeltasLookup from '@/components/cylc/common/deltas' import Alert from '@/model/Alert.model' import applyDeltasTree from '@/components/cylc/tree/deltas' diff --git a/src/store/workflows.module.js b/src/store/workflows.module.js index fd22126c1..552cea44d 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -14,7 +14,9 @@ * 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 Vue from 'vue' +import Alert from '@/model/Alert.model' +import applyDeltasGscan from '@/components/cylc/gscan/deltas' const state = { /** @@ -24,6 +26,9 @@ const state = { * @type {Object.} */ workflows: {}, + gscan: { + tree: {} + }, /** * 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 @@ -47,6 +52,17 @@ const getters = { } const mutations = { + SET_GSCAN (state, data) { + state.gscan = data + }, + CLEAR_GSCAN (state) { + Object.keys(state.gscan.tree).forEach(key => { + Vue.delete(state.gscan.tree, key) + }) + state.gscan = { + tree: {} + } + }, SET_WORKFLOW_NAME (state, data) { state.workflowName = data }, @@ -67,6 +83,25 @@ const actions = { }, clearWorkflows ({ commit }) { commit('SET_WORKFLOWS', []) + }, + applyGScanDeltas ({ commit, state }, data) { + // TODO + const gscan = state.gscan + const options = { + hierarchical: true + } + const result = applyDeltasGscan(data, gscan, options) + if (result.errors.length === 0) { + commit('SET_GSCAN', gscan) + } + result.errors.forEach(error => { + commit('SET_ALERT', new Alert(error[0], null, 'error'), { root: true }) + // eslint-disable-next-line no-console + console.warn(...error) + }) + }, + clearGScan ({ commit }) { + commit('CLEAR_GSCAN') } } From af5eabba7007a2c76783727ca0fc6a6e8a6a84ed Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Mon, 23 Aug 2021 14:46:04 +1200 Subject: [PATCH 04/19] Begin working on the functions to apply deltas (with propagated states!) to GScan --- src/components/cylc/gscan/GScan.vue | 30 ++++---- src/components/cylc/gscan/deltas.js | 106 ++++++++++++++++------------ src/components/cylc/gscan/index.js | 24 +++++++ src/store/workflows.module.js | 4 +- 4 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 src/components/cylc/gscan/index.js diff --git a/src/components/cylc/gscan/GScan.vue b/src/components/cylc/gscan/GScan.vue index 853de79d4..6e690ecf7 100644 --- a/src/components/cylc/gscan/GScan.vue +++ b/src/components/cylc/gscan/GScan.vue @@ -207,7 +207,7 @@ 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 { addNodeToTree, createWorkflowNode } from '@/components/cylc/gscan/nodes' import { filterHierarchically } from '@/components/cylc/gscan/filters' import GScanCallback from '@/components/cylc/gscan/callbacks' import { GSCAN_DELTAS_SUBSCRIPTION } from '@/graphql/queries' @@ -313,16 +313,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', ['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} */ @@ -351,7 +351,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.children || [], this.searchWorkflows, this.workflowStates, this.taskStates) } }, /** @@ -361,13 +361,13 @@ 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.children || [], newVal, this.workflowStates, this.taskStates) } }, - workflowNodes: { + gscan: { immediate: true, handler: function () { - this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, this.searchWorkflows, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree.children || [], this.searchWorkflows, this.workflowStates, this.taskStates) } } }, diff --git a/src/components/cylc/gscan/deltas.js b/src/components/cylc/gscan/deltas.js index 4785d1ec3..2968e2d72 100644 --- a/src/components/cylc/gscan/deltas.js +++ b/src/components/cylc/gscan/deltas.js @@ -14,80 +14,94 @@ * 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 { createWorkflowNode } from '@/components/cylc/gscan/nodes' +import { addWorkflow } from '@/components/cylc/gscan/index' -/** - * @param {DeltasAdded|Object} added - * @param {Array} workflows - * @return {Result} - */ -function applyDeltasAdded (added, workflows) { +function applyDeltasAdded (added, gscan, options) { const result = { errors: [] } - if (added && added.workflow && added.workflow.status) { + if (added.workflow) { + const hierarchical = options.hierarchical || true + const workflowNode = createWorkflowNode(added.workflow, hierarchical) try { - Vue.set(workflows, added.workflow.id, added.workflow) + addWorkflow(workflowNode, gscan, options) } catch (error) { result.errors.push([ 'Error applying GScan added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', error, - added, - workflows + added.workflow, + gscan, + options ]) } } return result } -/** - * @param {DeltasUpdated|Object} updated - * @param {Array} workflows - * @return {Result} - */ -function applyDeltasUpdated (updated, workflows) { +function applyDeltasUpdated () { const result = { errors: [] } - try { - if (updated && updated.workflow && workflows[updated.workflow.id]) { - mergeWith(workflows[updated.workflow.id], updated.workflow, mergeWithCustomizer) - } - } catch (error) { - result.errors.push([ - 'Error applying GScan updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', - error, - updated, - workflows - ]) + return result +} + +function applyDeltasPruned () { + const result = { + errors: [] } return result } +const DELTAS = { + added: applyDeltasAdded, + updated: applyDeltasUpdated, + pruned: applyDeltasPruned +} + /** - * @param {DeltasPruned|Object} pruned - * @param {Array} workflows - * @return {Result} + * @param {Deltas} deltas + * @param {*} gscan + * @param {*} options + * @returns {Result} */ -function applyDeltasPruned (pruned, workflows) { - const result = { - errors: [] +function handleDeltas (deltas, gscan, options) { + const errors = [] + Object.keys(DELTAS).forEach(key => { + if (deltas[key]) { + const handlingFunction = DELTAS[key] + const result = handlingFunction(deltas[key], gscan, options) + errors.push(...result.errors) + } + }) + return { + errors } +} + +/** + * @param {GraphQLResponseData} data + * @param {*} gscan + * @param {*}options + * @returns {Result} + */ +export default function (data, gscan, options) { + const deltas = data.deltas try { - if (pruned && pruned.workflow) { - Vue.delete(workflows, pruned.workflow) - } + return handleDeltas(deltas, gscan, options) } catch (error) { - result.errors.push([ - 'Error applying GScan pruned-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state', - error, - pruned, - workflows - ]) + return { + errors: [ + [ + 'Unexpected error applying gscan deltas', + error, + deltas, + gscan, + options + ] + ] + } } - return result } export { diff --git a/src/components/cylc/gscan/index.js b/src/components/cylc/gscan/index.js new file mode 100644 index 000000000..83060d8c4 --- /dev/null +++ b/src/components/cylc/gscan/index.js @@ -0,0 +1,24 @@ +/** + * 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 . + */ + +function addWorkflow (workflow, gscan, options) { + +} + +export { + addWorkflow +} diff --git a/src/store/workflows.module.js b/src/store/workflows.module.js index 552cea44d..67599a8bf 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -16,6 +16,7 @@ */ import Vue from 'vue' import Alert from '@/model/Alert.model' +import applyDeltasLookup from '@/components/cylc/common/deltas' import applyDeltasGscan from '@/components/cylc/gscan/deltas' const state = { @@ -78,14 +79,13 @@ 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', []) }, applyGScanDeltas ({ commit, state }, data) { - // TODO const gscan = state.gscan const options = { hierarchical: true From 5b63586859ffa23a8d9529b62181ab6da3dcc5cd Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Tue, 24 Aug 2021 10:23:31 +1200 Subject: [PATCH 05/19] Add a lookup to gscan structure (to access workflow when update/pruning it) --- src/store/workflows.module.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/store/workflows.module.js b/src/store/workflows.module.js index 67599a8bf..f7c66764f 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -27,8 +27,13 @@ const state = { * @type {Object.} */ workflows: {}, + /** + * 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. + */ gscan: { - tree: {} + tree: {}, + lookup: {} }, /** * This holds the name of the current workflow. This is set by VueRouter @@ -57,11 +62,14 @@ const mutations = { state.gscan = data }, CLEAR_GSCAN (state) { - Object.keys(state.gscan.tree).forEach(key => { - Vue.delete(state.gscan.tree, key) - }) + for (const property of ['tree', 'lookup']) { + Object.keys(state.gscan[property]).forEach(key => { + Vue.delete(state.gscan[property], key) + }) + } state.gscan = { - tree: {} + tree: {}, + lookup: {} } }, SET_WORKFLOW_NAME (state, data) { From b45a565a52882462284692bb05b441d4bd23a7c1 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Tue, 24 Aug 2021 10:34:01 +1200 Subject: [PATCH 06/19] Base work for handling deltas in GScan --- src/components/cylc/gscan/deltas.js | 80 +++++++++++++++++++++++++++-- src/components/cylc/gscan/index.js | 39 +++++++++++++- src/components/cylc/gscan/nodes.js | 12 ++--- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/src/components/cylc/gscan/deltas.js b/src/components/cylc/gscan/deltas.js index 2968e2d72..9038d2780 100644 --- a/src/components/cylc/gscan/deltas.js +++ b/src/components/cylc/gscan/deltas.js @@ -14,9 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import { addWorkflow, updateWorkflow, removeWorkflow } from '@/components/cylc/gscan/index' import { createWorkflowNode } from '@/components/cylc/gscan/nodes' -import { addWorkflow } from '@/components/cylc/gscan/index' +/** + * Deltas added. + * + * @param {DeltasAdded} added + * @param {GScan} gscan + * @param {*} options + * @returns {Result} + */ function applyDeltasAdded (added, gscan, options) { const result = { errors: [] @@ -39,17 +47,73 @@ function applyDeltasAdded (added, gscan, options) { return result } -function applyDeltasUpdated () { +/** + * Deltas updated. + * + * @param {DeltasUpdated} updated + * @param {GScan} gscan + * @param {*} options + * @returns {Result} + */ +function applyDeltasUpdated (updated, gscan, options) { const result = { errors: [] } + if (updated.workflow) { + const updatedData = updated.workflow + const hierarchical = options.hierarchical || true + try { + const existingData = gscan.lookup[updatedData.id] + if (!existingData) { + result.errors.push([ + `Updated node [${updatedData.id}] not found in workflow lookup`, + updatedData, + gscan, + options + ]) + } else { + const workflowNode = createWorkflowNode(updatedData, hierarchical) + updateWorkflow(workflowNode, 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, + updatedData, + gscan, + options + ]) + } + } return result } -function applyDeltasPruned () { +/** + * Deltas pruned. + * + * @param {DeltasPruned} pruned + * @param {GScan} gscan + * @param {*} options + * @returns {Result} + */ +function applyDeltasPruned (pruned, gscan, options) { const result = { errors: [] } + if (pruned.workflow) { + const workflowId = pruned.workflow + try { + 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 } @@ -60,8 +124,14 @@ const DELTAS = { } /** + * 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 + * @param {GScan} gscan * @param {*} options * @returns {Result} */ @@ -82,7 +152,7 @@ function handleDeltas (deltas, gscan, options) { /** * @param {GraphQLResponseData} data * @param {*} gscan - * @param {*}options + * @param {*} options * @returns {Result} */ export default function (data, gscan, options) { diff --git a/src/components/cylc/gscan/index.js b/src/components/cylc/gscan/index.js index 83060d8c4..8f6c56ef9 100644 --- a/src/components/cylc/gscan/index.js +++ b/src/components/cylc/gscan/index.js @@ -15,10 +15,47 @@ * along with this program. If not, see . */ +/** + * @typedef {Object} GScan + */ + +/** + * @typedef {Object} Lookup + */ + +/** + * @typedef {Object} Tree + */ + +/** + * @param {WorkflowGraphQLData} workflow + * @param {GScan} gscan + * @param {*} options + */ function addWorkflow (workflow, gscan, options) { } +/** + * @param {WorkflowGraphQLData} workflow + * @param {GScan} gscan + * @param {*} options + */ +function updateWorkflow (workflow, gscan, options) { + +} + +/** + * @param {WorkflowGraphQLData} workflow + * @param {GScan} gscan + * @param {*} options + */ +function removeWorkflow (workflow, gscan, options) { + +} + export { - addWorkflow + addWorkflow, + updateWorkflow, + removeWorkflow } diff --git a/src/components/cylc/gscan/nodes.js b/src/components/cylc/gscan/nodes.js index 524257d42..5af91990a 100644 --- a/src/components/cylc/gscan/nodes.js +++ b/src/components/cylc/gscan/nodes.js @@ -19,7 +19,7 @@ import { sortedIndexBy } from '@/components/cylc/common/sort' import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/sort' /** - * @typedef {Object} WorkflowGScanNode + * @typedef {Object} TreeNode * @property {String} id * @property {String} name * @property {String} type @@ -27,11 +27,11 @@ import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/ */ /** - * @typedef {Object} WorkflowNamePartGScanNode - * @property {String} id - * @property {String} name - * @property {String} type - * @property {WorkflowGraphQLData} node + * @typedef {TreeNode} WorkflowGScanNode + */ + +/** + * @typedef {TreeNode} WorkflowNamePartGScanNode * @property {Array} children */ From 22b1681465f5b9e1ba72a4796d847c7d11ae6b4f Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Tue, 24 Aug 2021 15:40:52 +1200 Subject: [PATCH 07/19] Handle adding nodes --- src/components/cylc/gscan/GScan.vue | 9 ++-- src/components/cylc/gscan/deltas.js | 11 ++--- src/components/cylc/gscan/index.js | 65 ++++++++++++++++++++++++--- src/components/cylc/gscan/nodes.js | 2 +- src/components/cylc/tree/TreeItem.vue | 4 ++ src/components/cylc/tree/nodes.js | 4 +- src/store/workflows.module.js | 4 +- 7 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/components/cylc/gscan/GScan.vue b/src/components/cylc/gscan/GScan.vue index 6e690ecf7..f7b8d123b 100644 --- a/src/components/cylc/gscan/GScan.vue +++ b/src/components/cylc/gscan/GScan.vue @@ -313,7 +313,7 @@ export default { } }, computed: { - ...mapState('workflows', ['workflows', 'gscan']), + ...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 @@ -351,7 +351,7 @@ export default { deep: true, immediate: false, handler: function (newVal) { - this.filteredWorkflows = this.filterHierarchically(this.gscan.tree.children || [], this.searchWorkflows, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, this.searchWorkflows, this.workflowStates, this.taskStates) } }, /** @@ -361,13 +361,14 @@ export default { searchWorkflows: { immediate: false, handler: function (newVal) { - this.filteredWorkflows = this.filterHierarchically(this.gscan.tree.children || [], newVal, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, newVal, this.workflowStates, this.taskStates) } }, gscan: { immediate: true, + deep: true, handler: function () { - this.filteredWorkflows = this.filterHierarchically(this.gscan.tree.children || [], this.searchWorkflows, this.workflowStates, this.taskStates) + this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, this.searchWorkflows, this.workflowStates, this.taskStates) } } }, diff --git a/src/components/cylc/gscan/deltas.js b/src/components/cylc/gscan/deltas.js index 9038d2780..e7895db07 100644 --- a/src/components/cylc/gscan/deltas.js +++ b/src/components/cylc/gscan/deltas.js @@ -21,7 +21,7 @@ import { createWorkflowNode } from '@/components/cylc/gscan/nodes' * Deltas added. * * @param {DeltasAdded} added - * @param {GScan} gscan + * @param {import('./index').GScan} gscan * @param {*} options * @returns {Result} */ @@ -51,7 +51,7 @@ function applyDeltasAdded (added, gscan, options) { * Deltas updated. * * @param {DeltasUpdated} updated - * @param {GScan} gscan + * @param {import('./index').GScan} gscan * @param {*} options * @returns {Result} */ @@ -61,7 +61,6 @@ function applyDeltasUpdated (updated, gscan, options) { } if (updated.workflow) { const updatedData = updated.workflow - const hierarchical = options.hierarchical || true try { const existingData = gscan.lookup[updatedData.id] if (!existingData) { @@ -72,7 +71,8 @@ function applyDeltasUpdated (updated, gscan, options) { options ]) } else { - const workflowNode = createWorkflowNode(updatedData, hierarchical) + // TODO: hierarchy is always false here? + const workflowNode = createWorkflowNode(updatedData, false) updateWorkflow(workflowNode, gscan, options) } } catch (error) { @@ -92,7 +92,7 @@ function applyDeltasUpdated (updated, gscan, options) { * Deltas pruned. * * @param {DeltasPruned} pruned - * @param {GScan} gscan + * @param {import('./index').GScan} gscan * @param {*} options * @returns {Result} */ @@ -100,6 +100,7 @@ function applyDeltasPruned (pruned, gscan, options) { const result = { errors: [] } + // TODO: why not pruned.workflows??? if (pruned.workflow) { const workflowId = pruned.workflow try { diff --git a/src/components/cylc/gscan/index.js b/src/components/cylc/gscan/index.js index 8f6c56ef9..a01be6f09 100644 --- a/src/components/cylc/gscan/index.js +++ b/src/components/cylc/gscan/index.js @@ -17,27 +17,80 @@ /** * @typedef {Object} GScan + * @property {Array} tree + * @property {Lookup} lookup */ /** * @typedef {Object} Lookup */ -/** - * @typedef {Object} Tree - */ +import { sortedIndexBy } from '@/components/cylc/common/sort' +import { sortWorkflowNamePartNodeOrWorkflowNode } from '@/components/cylc/gscan/sort' /** - * @param {WorkflowGraphQLData} workflow + * @param {TreeNode} workflow * @param {GScan} gscan * @param {*} options */ function addWorkflow (workflow, gscan, options) { + const hierarchical = options.hierarchical || true + if (hierarchical) { + addHierarchicalWorkflow(workflow, gscan.lookup, gscan.tree, options) + } else { + gscan.lookup[workflow.id] = workflow + gscan.tree.push(workflow) + } +} +/** + * @param workflow + * @param {Lookup} lookup + * @param {Array} tree + * @param {*} options + */ +function addHierarchicalWorkflow (workflow, lookup, tree, options) { + if (!lookup[workflow.id]) { + // a new node, let's add this node and its descendants to the lookup + lookup[workflow.id] = workflow + if (workflow.children) { + const stack = [...workflow.children] + while (stack.length) { + const currentNode = stack.shift() + lookup[currentNode.id] = currentNode + if (currentNode.children) { + stack.push(...currentNode.children) + } + } + } + // and now add the top-level 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 element, something like + // `sortedIndex = (array.length - sortedIndex)`. + const sortedIndex = sortedIndexBy( + tree, + workflow, + (n) => n.name, + sortWorkflowNamePartNodeOrWorkflowNode + ) + tree.splice(sortedIndex, 0, workflow) + } else { + // we will have to merge the hierarchies + const existingNode = lookup[workflow.id] + // TODO: combine states summaries? + // Copy array since we will iterate it, and modify existingNode.children + if (existingNode.children) { + const children = [...workflow.children] + for (const child of children) { + // Recursion + addHierarchicalWorkflow(child, lookup, existingNode.children, options) + } + } + } } /** - * @param {WorkflowGraphQLData} workflow + * @param {TreeNode} workflow * @param {GScan} gscan * @param {*} options */ @@ -46,7 +99,7 @@ function updateWorkflow (workflow, gscan, options) { } /** - * @param {WorkflowGraphQLData} workflow + * @param {TreeNode} workflow * @param {GScan} gscan * @param {*} options */ diff --git a/src/components/cylc/gscan/nodes.js b/src/components/cylc/gscan/nodes.js index 5af91990a..d0373ee9f 100644 --- a/src/components/cylc/gscan/nodes.js +++ b/src/components/cylc/gscan/nodes.js @@ -85,7 +85,7 @@ function newWorkflowPartNode (id, part) { * * @param {WorkflowGraphQLData} workflow * @param {boolean} hierarchy - whether to parse the Workflow name and create a hierarchy or not - * @returns {WorkflowGScanNode|WorkflowNamePartGScanNode|null} + * @returns {TreeNode} */ function createWorkflowNode (workflow, hierarchy) { if (!hierarchy) { diff --git a/src/components/cylc/tree/TreeItem.vue b/src/components/cylc/tree/TreeItem.vue index df49fd1c3..645fd67c3 100644 --- a/src/components/cylc/tree/TreeItem.vue +++ b/src/components/cylc/tree/TreeItem.vue @@ -284,8 +284,12 @@ export default { } }, created () { + // console.log(`TreeItem ${this.node.id} created!`) this.$emit('tree-item-created', this) }, + updated () { + // console.log(`TreeItem ${this.node.id} updated!`) + }, beforeDestroy () { this.$emit('tree-item-destroyed', this) }, 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/store/workflows.module.js b/src/store/workflows.module.js index f7c66764f..528eef457 100644 --- a/src/store/workflows.module.js +++ b/src/store/workflows.module.js @@ -32,7 +32,7 @@ const state = { * and the lookup is a helper structure for quick access to nodes in the tree. */ gscan: { - tree: {}, + tree: [], lookup: {} }, /** @@ -68,7 +68,7 @@ const mutations = { }) } state.gscan = { - tree: {}, + tree: [], lookup: {} } }, From fdf427a36a25b61d51f6cf2c18c38dade6c77973 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Tue, 24 Aug 2021 21:37:46 +1200 Subject: [PATCH 08/19] A few performance improvements for TreeItem --- src/components/cylc/tree/TreeItem.vue | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/cylc/tree/TreeItem.vue b/src/components/cylc/tree/TreeItem.vue index 645fd67c3..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 } }, @@ -284,12 +284,8 @@ export default { } }, created () { - // console.log(`TreeItem ${this.node.id} created!`) this.$emit('tree-item-created', this) }, - updated () { - // console.log(`TreeItem ${this.node.id} updated!`) - }, beforeDestroy () { this.$emit('tree-item-destroyed', this) }, From 505d7328afd4328df825385013c8febc6befc0a0 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Tue, 24 Aug 2021 22:08:53 +1200 Subject: [PATCH 09/19] Avoid undefined error in Tree component --- src/components/cylc/tree/Tree.vue | 3 +++ 1 file changed, 3 insertions(+) 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 }) From 6e0d9d70afe2af89efb8794e23868c97131ded70 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Wed, 8 Sep 2021 17:02:01 +1200 Subject: [PATCH 10/19] Create WorkflowStateSummary component --- src/components/cylc/gscan/GScan.vue | 80 ++----------- .../cylc/gscan/WorkflowStateSummary.vue | 110 ++++++++++++++++++ 2 files changed, 117 insertions(+), 73 deletions(-) create mode 100644 src/components/cylc/gscan/WorkflowStateSummary.vue diff --git a/src/components/cylc/gscan/GScan.vue b/src/components/cylc/gscan/GScan.vue index f7b8d123b..ac6419247 100644 --- a/src/components/cylc/gscan/GScan.vue +++ b/src/components/cylc/gscan/GScan.vue @@ -143,42 +143,12 @@ along with this program. If not, see . - - - - - - - {{ countTasksInState(scope.node.node, state) }} {{ state }}. Recent {{ state }} tasks: -
- - {{ task }}
-
-
-
-
+
@@ -204,9 +174,9 @@ 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 WorkflowStateSummary from '@/components/cylc/gscan/WorkflowStateSummary' // import { addNodeToTree, createWorkflowNode } from '@/components/cylc/gscan/nodes' import { filterHierarchically } from '@/components/cylc/gscan/filters' import GScanCallback from '@/components/cylc/gscan/callbacks' @@ -215,9 +185,9 @@ import { GSCAN_DELTAS_SUBSCRIPTION } from '@/graphql/queries' export default { name: 'GScan', components: { - Job, Tree, - WorkflowIcon + WorkflowIcon, + WorkflowStateSummary }, mixins: [ subscriptionComponentMixin @@ -416,42 +386,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..3416004c7 --- /dev/null +++ b/src/components/cylc/gscan/WorkflowStateSummary.vue @@ -0,0 +1,110 @@ + + + + + From 4d1a23c58e7f7fe6fa58433f9647abc3151b5672 Mon Sep 17 00:00:00 2001 From: "Bruno P. Kinoshita" Date: Thu, 9 Sep 2021 09:39:12 +1200 Subject: [PATCH 11/19] Simplify the WorkflowStateSummary component, replace methods by computed, use more props --- src/components/cylc/gscan/GScan.vue | 4 +- .../cylc/gscan/WorkflowStateSummary.vue | 83 ++++++++++++------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/components/cylc/gscan/GScan.vue b/src/components/cylc/gscan/GScan.vue index ac6419247..c10950395 100644 --- a/src/components/cylc/gscan/GScan.vue +++ b/src/components/cylc/gscan/GScan.vue @@ -147,7 +147,9 @@ along with this program. If not, see . class="text-right c-gscan-workflow-states" > diff --git a/src/components/cylc/gscan/WorkflowStateSummary.vue b/src/components/cylc/gscan/WorkflowStateSummary.vue index 3416004c7..4d641d674 100644 --- a/src/components/cylc/gscan/WorkflowStateSummary.vue +++ b/src/components/cylc/gscan/WorkflowStateSummary.vue @@ -19,9 +19,9 @@ along with this program. If not, see . - {{ countTasksInState(node.node, state) }} {{ state }}. Recent {{ state }} tasks: + {{ countTasksInState(state) }} {{ state }}. Recent {{ state }} tasks:
{{ task }}
@@ -56,10 +56,33 @@ along with this program. If not, see .