From c6ae6aebef8a76406a0e798f1911b4b8c5507d44 Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:10:47 +0000 Subject: [PATCH 1/3] Simplify TreeItem and fix tests --- cypress/component/treeItem.cy.js | 79 +++++++++++++++++++ src/components/cylc/tree/TreeItem.vue | 41 ++++------ 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, 156 insertions(+), 37 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..fc052f1c5 --- /dev/null +++ b/cypress/component/treeItem.cy.js @@ -0,0 +1,79 @@ +/* + * 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' + +// Get lists of: a) all tree item nodes; b) only visible ones. +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 } + }) +}) + +// Expand/collapse the first node of this type. +Cypress.Commands.add('toggleNode', (nodeType) => { + cy.get(`[data-node-type=${nodeType}] .node-expand-collapse-button:first`).click() +}) + +describe('TreeItem component', () => { + it('expands/collapses 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 nodes 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 8a7ecad70..849f54727 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" @@ -134,7 +135,7 @@ along with this program. If not, see . class="ml-2 bg-grey text-white" size="small" link - @click="toggleExpandCollapse" + @click="toggleExpandCollapse()" > +{{ jobMessageOutputs.length - 5 }} @@ -184,6 +185,8 @@ import { jobMessageOutputs } from '@/utils/tasks' import { getIndent, getNodeChildren } from '@/components/cylc/tree/util' +import { once } from '@/utils' +import { useToggle } from '@vueuse/core' export default { name: 'TreeItem', @@ -240,27 +243,22 @@ export default { }, }, - data () { + setup (props) { + const [isExpanded, toggleExpandCollapse] = useToggle( + props.autoExpandTypes.includes(props.node.type) + ) + // Toggles to true when this.isExpanded first becomes true and doesn't get recomputed afterwards + const renderChildren = once(isExpanded) + return { - manuallyExpanded: null, + isExpanded, + latestJob, + renderChildren, + toggleExpandCollapse, } }, computed: { - isExpanded: { - get () { - return this.manuallyExpanded ?? this.autoExpandTypes.includes(this.node.type) - }, - set (value) { - this.manuallyExpanded = value - } - }, - - renderChildren () { - // Toggles to true when this.isExpanded first becomes true and doesn't get recomputed afterwards - return this.renderChildren || this.isExpanded - }, - hasChildren () { return ( // "job" nodes have auto-generated "job-detail" nodes @@ -310,13 +308,6 @@ export default { } }, - methods: { - toggleExpandCollapse () { - this.isExpanded = !this.isExpanded - }, - latestJob - }, - icons: { mdiChevronRight, }, diff --git a/src/utils/index.js b/src/utils/index.js index 0d794751b..2d476a683 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { watch } from 'vue' +import { ref, watch } from 'vue' /** * Watch source until it is truthy, then call the callback (and stop watching). @@ -27,6 +27,10 @@ import { watch } from 'vue' * @param {import('vue').WatchOptions?} options */ export function when (source, callback, options = {}) { + if (source.value) { + callback() + return + } const unwatch = watch( source, (ready) => { @@ -35,7 +39,7 @@ export function when (source, callback, options = {}) { callback() } }, - { immediate: true, ...options } + options ) } @@ -53,3 +57,20 @@ export function until (source, options = {}) { when(source, resolve, options) }) } + +/** + * Return a ref that is permanently set to true when the source becomes truthy. + * + * @param {import('vue').WatchSource} source + * @param {import('vue').WatchOptions?} options + * @returns {import('vue').Ref} + */ +export function once (source, options = {}) { + const _ref = ref(false) + when( + source, + () => { _ref.value = true }, + options + ) + return _ref +} diff --git a/tests/unit/components/cylc/tree/tree.data.js b/tests/unit/components/cylc/tree/tree.data.js index 67e63fd18..c19834897 100644 --- a/tests/unit/components/cylc/tree/tree.data.js +++ b/tests/unit/components/cylc/tree/tree.data.js @@ -237,7 +237,7 @@ const simpleWorkflowTree4Nodes = [ __typename: 'CyclePoint', state: 'failed' }, - children: [], + children: ['stub'], familyTree: [ { id: '~user/workflow1//20100101T0000Z/root', diff --git a/tests/unit/components/cylc/tree/treeitem.vue.spec.js b/tests/unit/components/cylc/tree/treeitem.vue.spec.js index 113490c75..ae1310b3e 100644 --- a/tests/unit/components/cylc/tree/treeitem.vue.spec.js +++ b/tests/unit/components/cylc/tree/treeitem.vue.spec.js @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) NIWA & British Crown (Met Office) & Contributors. * * This program is free software: you can redistribute it and/or modify @@ -116,18 +116,17 @@ describe('TreeItem component', () => { describe('children', () => { it.each([ - { manuallyExpanded: null, expected: ['CyclePoint', 'TaskProxy'] }, - { manuallyExpanded: true, expected: ['CyclePoint', 'TaskProxy', 'Job'] }, - { manuallyExpanded: false, expected: [] }, - ])('recursively mounts child TreeItems if expanded ($manuallyExpanded)', ({ manuallyExpanded, expected }) => { + { autoExpandTypes: undefined, expected: ['CyclePoint', 'TaskProxy'] }, + { autoExpandTypes: ['workflow', 'cycle', 'family', 'task'], expected: ['CyclePoint', 'TaskProxy', 'Job'] }, + { autoExpandTypes: ['workflow'], expected: ['CyclePoint'] }, + { autoExpandTypes: [], expected: [] }, + ])('recursively mounts child TreeItems ($autoExpandTypes)', ({ autoExpandTypes, expected }) => { const wrapper = mountFunction({ props: { node: simpleWorkflowNode, filteredOutNodesCache: new WeakMap(), + autoExpandTypes, }, - data: () => ({ - manuallyExpanded, - }), }) expect( wrapper.findAllComponents({ name: 'TreeItem' }) diff --git a/tests/unit/utils/index.spec.js b/tests/unit/utils/index.spec.js index 25fa72bf5..a1f99afc8 100644 --- a/tests/unit/utils/index.spec.js +++ b/tests/unit/utils/index.spec.js @@ -16,7 +16,7 @@ */ import { nextTick, ref } from 'vue' -import { when, until } from '@/utils/index' +import { once, when, until } from '@/utils/index' describe.each([ { func: when, description: 'watches source until true and then stops watching' }, @@ -42,3 +42,32 @@ describe.each([ expect(counter).toEqual(1) }) }) + +describe('when()', () => { + it('works for a source that is already truthy', () => { + const source = ref(true) + let counter = 0 + when(source, () => counter++) + expect(counter).toEqual(1) + }) +}) + +describe('once()', () => { + it('returns a ref that permanently toggles to true when the source bevomes truthy', async () => { + const source = ref(false) + const myRef = once(source) + expect(myRef.value).toEqual(false) + source.value = true + await nextTick() + expect(myRef.value).toEqual(true) + source.value = false + await nextTick() + expect(myRef.value).toEqual(true) + }) + + it('works for a source that is already truthy', () => { + const source = ref(true) + const myRef = once(source) + expect(myRef.value).toEqual(true) + }) +}) From 6de9d53e6ff3fe5725b5f056124959af9d77739f Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:10:47 +0000 Subject: [PATCH 2/3] Show flow nums Show flow nums in tree/table views if not 1 or None. Dim flow=None in tree/table/graph views. Show all flow nums in command menu. Also update mock data. --- src/components/cylc/commandMenu/Menu.vue | 13 +++- src/components/cylc/common/FlowNumsChip.vue | 56 ++++++++++++++ src/components/cylc/table/Table.vue | 20 ++++- src/components/cylc/tree/TreeItem.vue | 20 ++++- .../mock/checkpoint/get_checkpoint.py | 1 + src/services/mock/json/workflows/multi.json | 2 + src/services/mock/json/workflows/one.json | 25 ++++++- src/styles/cylc/_tree.scss | 1 + src/styles/index.scss | 6 ++ src/utils/tasks.js | 34 ++++++--- src/views/Graph.vue | 6 +- src/views/Table.vue | 1 + src/views/Tree.vue | 1 + tests/e2e/specs/aotf.cy.js | 26 +++---- tests/e2e/specs/graph.cy.js | 14 ++++ tests/e2e/specs/table.cy.js | 38 ++++++---- tests/e2e/specs/tree.cy.js | 73 +++++++++++-------- .../cylc/common/flowNumsChip.vue.spec.js | 37 ++++++++++ .../components/cylc/tree/tree.vue.spec.js | 11 +-- tests/unit/utils/tasks.spec.js | 31 +++++++- 20 files changed, 319 insertions(+), 97 deletions(-) create mode 100644 src/components/cylc/common/FlowNumsChip.vue create mode 100644 tests/unit/components/cylc/common/flowNumsChip.vue.spec.js 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/common/FlowNumsChip.vue b/src/components/cylc/common/FlowNumsChip.vue new file mode 100644 index 000000000..38fb09b9b --- /dev/null +++ b/src/components/cylc/common/FlowNumsChip.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/cylc/table/Table.vue b/src/components/cylc/table/Table.vue index c6390c10b..fd2c6413b 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" > - + +