From 5c8459a9a21c29f12fd6bd4e0716942bff3e1a32 Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:51:37 +0000 Subject: [PATCH 1/3] Show flow nums in tree view & command menu --- src/components/cylc/commandMenu/Menu.vue | 13 ++++++--- src/components/cylc/tree/TreeItem.vue | 28 ++++++++++++++++--- src/services/mock/json/workflows/one.json | 7 +++++ src/styles/cylc/_tree.scss | 1 + src/utils/tasks.js | 22 +++++++-------- src/views/Tree.vue | 1 + .../components/cylc/tree/tree.vue.spec.js | 11 ++------ tests/unit/utils/tasks.spec.js | 19 ++++++++++++- 8 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/components/cylc/commandMenu/Menu.vue b/src/components/cylc/commandMenu/Menu.vue index a3e532ce9..8f8a9a849 100644 --- a/src/components/cylc/commandMenu/Menu.vue +++ b/src/components/cylc/commandMenu/Menu.vue @@ -126,6 +126,8 @@ import { mapGetters, mapState } from 'vuex' import WorkflowState from '@/model/WorkflowState.model' import { eventBus } from '@/services/eventBus' import CopyBtn from '@/components/core/CopyBtn.vue' +import { upperFirst } from 'lodash-es' +import { formatFlowNums } from '@/utils/tasks' export default { name: 'CommandMenu', @@ -199,14 +201,14 @@ export default { // can happen briefly when switching workflows return } - let ret = this.node.type + let ret = upperFirst(this.node.type) if (this.node.type !== 'cycle') { // NOTE: cycle point nodes don't have associated node data at present - ret += ' - ' + ret += ' • ' if (this.node.type === 'workflow') { - ret += this.node.node.statusMsg || this.node.node.status || 'state unknown' + ret += upperFirst(this.node.node.statusMsg || this.node.node.status || 'state unknown') } else { - ret += this.node.node.state || 'state unknown' + ret += upperFirst(this.node.node.state || 'state unknown') if (this.node.node.isHeld) { ret += ' (held)' } @@ -216,6 +218,9 @@ export default { if (this.node.node.isRunahead) { ret += ' (runahead)' } + if (this.node.node.flowNums) { + ret += ` • Flows: ${formatFlowNums(this.node.node.flowNums)}` + } } } return ret diff --git a/src/components/cylc/tree/TreeItem.vue b/src/components/cylc/tree/TreeItem.vue index 8a7ecad70..dc210d593 100644 --- a/src/components/cylc/tree/TreeItem.vue +++ b/src/components/cylc/tree/TreeItem.vue @@ -85,6 +85,19 @@ along with this program. If not, see . /> {{ node.name }} + + {{ formatFlowNums(node.node.flowNums) }} + + Flows: {{ formatFlowNums(node.node.flowNums) }} + + diff --git a/src/services/mock/json/workflows/one.json b/src/services/mock/json/workflows/one.json index 45ea00f2b..7ba953204 100644 --- a/src/services/mock/json/workflows/one.json +++ b/src/services/mock/json/workflows/one.json @@ -115,6 +115,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/root", "name": "root", @@ -133,6 +134,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/SUCCEEDED", "name": "SUCCEEDED", @@ -152,6 +154,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/BAD", "name": "BAD", @@ -170,6 +173,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/BAD", "name": "BAD", @@ -188,6 +192,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/root", "name": "root", @@ -206,6 +211,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/SUCCEEDED", "name": "SUCCEEDED", @@ -225,6 +231,7 @@ "isQueued": false, "isRunahead": false, "cyclePoint": "20000102T0000Z", + "flowNums": "[1]", "firstParent": { "id": "~user/one//20000102T0000Z/root", "name": "root", diff --git a/src/styles/cylc/_tree.scss b/src/styles/cylc/_tree.scss index d80b9d64c..573e7a622 100644 --- a/src/styles/cylc/_tree.scss +++ b/src/styles/cylc/_tree.scss @@ -97,6 +97,7 @@ $icon-width: 1.5rem; .node-data { display: flex; flex-wrap: nowrap; + align-items: center; .node-summary { display: flex; flex-wrap: nowrap; diff --git a/src/utils/tasks.js b/src/utils/tasks.js index 370edf39c..53ac914a2 100644 --- a/src/utils/tasks.js +++ b/src/utils/tasks.js @@ -40,7 +40,7 @@ const isStoppedOrderedStates = [ * @returns {string} a valid Task State name, or empty string if not found * @link @see https://github.com/cylc/cylc-flow/blob/d66ae5c3ce8c749c8178d1cd53cb8c81d1560346/lib/cylc/task_state_prop.py */ -function extractGroupState (childStates, isStopped = false) { +export function extractGroupState (childStates, isStopped = false) { const states = isStopped ? isStoppedOrderedStates : TaskState.enumValues for (const state of states) { if (childStates.includes(state.name)) { @@ -50,7 +50,7 @@ function extractGroupState (childStates, isStopped = false) { return '' } -function latestJob (taskProxy) { +export function latestJob (taskProxy) { return taskProxy?.children?.[0]?.node } @@ -67,7 +67,7 @@ function latestJob (taskProxy) { * } * } */ -function jobMessageOutputs (jobNode) { +export function jobMessageOutputs (jobNode) { const ret = [] for (const message of jobNode.node.messages || []) { @@ -96,7 +96,7 @@ function jobMessageOutputs (jobNode) { * 00:00:00, rather than undefined * @return {string=} Formatted duration */ -function formatDuration (dur, allowZeros = false) { +export function formatDuration (dur, allowZeros = false) { if (dur || (dur === 0 && allowZeros === true)) { const seconds = dur % 60 const minutes = ((dur - seconds) / 60) % 60 @@ -118,16 +118,16 @@ function formatDuration (dur, allowZeros = false) { return undefined } -function dtMean (taskNode) { +export function dtMean (taskNode) { // Convert to an easily read duration format: const dur = taskNode.node?.task?.meanElapsedTime return formatDuration(dur) } -export { - extractGroupState, - latestJob, - jobMessageOutputs, - formatDuration, - dtMean +/** + * @param {string} flowNums - Flow numbers in DB format + * @returns {string} - Flow numbers in pretty format + */ +export function formatFlowNums (flowNums) { + return JSON.parse(flowNums).join(', ') || 'None' } diff --git a/src/views/Tree.vue b/src/views/Tree.vue index d6a029c45..83dbb964d 100644 --- a/src/views/Tree.vue +++ b/src/views/Tree.vue @@ -193,6 +193,7 @@ fragment TaskProxyData on TaskProxy { firstParent { id } + flowNums } fragment JobData on Job { diff --git a/tests/unit/components/cylc/tree/tree.vue.spec.js b/tests/unit/components/cylc/tree/tree.vue.spec.js index 94abbe904..060fff6c7 100644 --- a/tests/unit/components/cylc/tree/tree.vue.spec.js +++ b/tests/unit/components/cylc/tree/tree.vue.spec.js @@ -15,28 +15,21 @@ * along with this program. If not, see . */ -// we mount the tree to include the TreeItem component and other vuetify children components import { mount } from '@vue/test-utils' import { vi } from 'vitest' -import { createVuetify } from 'vuetify' import { cloneDeep } from 'lodash' import Tree from '@/components/cylc/tree/Tree.vue' import { simpleWorkflowTree4Nodes } from './tree.data' -import CommandMenuPlugin from '@/components/cylc/commandMenu/plugin' - -const vuetify = createVuetify() describe('Tree component', () => { const mountFunction = (props) => mount(Tree, { - global: { - plugins: [vuetify, CommandMenuPlugin], - }, props: { workflows: cloneDeep(simpleWorkflowTree4Nodes), autoStripTypes: ['workflow'], filterState: null, ...props, - } + }, + shallow: true, }) it.each([ diff --git a/tests/unit/utils/tasks.spec.js b/tests/unit/utils/tasks.spec.js index 0511e3f36..7a7f66c36 100644 --- a/tests/unit/utils/tasks.spec.js +++ b/tests/unit/utils/tasks.spec.js @@ -16,7 +16,14 @@ */ import TaskState from '@/model/TaskState.model' -import { dtMean, extractGroupState, latestJob, formatDuration, jobMessageOutputs } from '@/utils/tasks' +import { + dtMean, + extractGroupState, + latestJob, + formatDuration, + jobMessageOutputs, + formatFlowNums, +} from '@/utils/tasks' describe('tasks', () => { describe('extractGroupState', () => { @@ -207,4 +214,14 @@ describe('tasks', () => { ]) }) }) + + describe('formatFlowNums', () => { + it.each([ + ['[1]', '1'], + ['[1, 4, 8]', '1, 4, 8'], + ['[]', 'None'], + ])('formatFlowNums(%s) -> %s', (input, expected) => { + expect(formatFlowNums(input)).toEqual(expected) + }) + }) }) From 929fefc84787051c58654d0da788ef3177275c33 Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:51:37 +0000 Subject: [PATCH 2/3] Simplify TreeItem and fix tests --- cypress/component/treeItem.cy.js | 77 +++++++++++++++++++ src/components/cylc/tree/TreeItem.vue | 43 ++++------- src/utils/index.js | 25 +++++- tests/unit/components/cylc/tree/tree.data.js | 2 +- .../components/cylc/tree/treeitem.vue.spec.js | 15 ++-- tests/unit/utils/index.spec.js | 31 +++++++- 6 files changed, 155 insertions(+), 38 deletions(-) create mode 100644 cypress/component/treeItem.cy.js diff --git a/cypress/component/treeItem.cy.js b/cypress/component/treeItem.cy.js new file mode 100644 index 000000000..1faeba449 --- /dev/null +++ b/cypress/component/treeItem.cy.js @@ -0,0 +1,77 @@ +/* + * 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 TreeItem from '@/components/cylc/tree/TreeItem.vue' +import { + simpleCyclepointNode, +} from '$tests/unit/components/cylc/tree/tree.data' + +Cypress.Commands.add('getNodeTypes', () => { + cy.get('.c-treeitem') + .then(($els) => { + const all = Array.from($els, ({ dataset }) => dataset.nodeType) + const visible = all.filter((val, i) => $els[i].checkVisibility()) + return { all, visible } + }) +}) + +Cypress.Commands.add('toggleNode', (nodeType) => { + cy.get(`[data-node-type=${nodeType}] .node-expand-collapse-button:first`).click() +}) + +describe('TreeItem component', () => { + it('children', () => { + cy.vmount(TreeItem, { + props: { + node: simpleCyclepointNode, + filteredOutNodesCache: new WeakMap(), + }, + }) + cy.addVuetifyStyles(cy) + + cy.getNodeTypes() + .should('deep.equal', { + // Auto expand everything down to task nodes by default + all: ['cycle', 'task'], + visible: ['cycle', 'task'] + }) + + cy.toggleNode('task') + cy.getNodeTypes() + .should('deep.equal', { + all: ['cycle', 'task', 'job'], + visible: ['cycle', 'task', 'job'] + }) + + cy.toggleNode('cycle') + cy.getNodeTypes() + .should('deep.equal', { + // All previously expanded ndoes under cycle should be hidden but remain rendered + all: ['cycle', 'task', 'job'], + visible: ['cycle'] + }) + + cy.toggleNode('cycle') + cy.toggleNode('job') + cy.getNodeTypes() + .should('deep.equal', { + // Job node does not use a child TreeItem + all: ['cycle', 'task', 'job'], + visible: ['cycle', 'task', 'job'] + }) + }) +}) diff --git a/src/components/cylc/tree/TreeItem.vue b/src/components/cylc/tree/TreeItem.vue index dc210d593..7de1256c7 100644 --- a/src/components/cylc/tree/TreeItem.vue +++ b/src/components/cylc/tree/TreeItem.vue @@ -19,6 +19,7 @@ along with this program. If not, see .
. v-if="renderExpandCollapseBtn" aria-label="Expand/collapse" class="node-expand-collapse-button flex-shrink-0" - @click="toggleExpandCollapse" + @click="toggleExpandCollapse()" :style="expandCollapseBtnStyle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" @@ -86,6 +87,7 @@ along with this program. If not, see .
{{ node.name }} . class="ml-2 bg-grey text-white" size="small" link - @click="toggleExpandCollapse" + @click="toggleExpandCollapse()" > +{{ jobMessageOutputs.length - 5 }} @@ -188,7 +190,6 @@ along with this program. If not, see . diff --git a/src/components/cylc/table/Table.vue b/src/components/cylc/table/Table.vue index c6390c10b..b834c598b 100644 --- a/src/components/cylc/table/Table.vue +++ b/src/components/cylc/table/Table.vue @@ -28,7 +28,11 @@ along with this program. If not, see . v-model:items-per-page="itemsPerPage" > - + +