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 @@
+
+
+
+
+ {{ flowNumsStr }}
+
+ Flows: {{ flowNumsStr }}
+
+
+
+
+
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"
>
-
+
.
:previous-state="item.previousJob?.node?.state"
/>
-
{{ item.task.name }}
+ {{ item.task.name }}
+
@@ -110,13 +118,17 @@ import {
datetimeComparator,
numberComparator,
} from '@/components/cylc/table/sort'
-import { dtMean } from '@/utils/tasks'
+import {
+ dtMean,
+ isFlowNone,
+} from '@/utils/tasks'
import { useCyclePointsOrderDesc } from '@/composables/localStorage'
import {
initialOptions,
updateInitialOptionsEvent,
useInitialOptions
} from '@/utils/initialOptions'
+import FlowNumsChip from '@/components/cylc/common/FlowNumsChip.vue'
export default {
name: 'TableComponent',
@@ -132,6 +144,7 @@ export default {
},
components: {
+ FlowNumsChip,
Task,
Job,
},
@@ -243,6 +256,7 @@ export default {
icons: {
mdiChevronDown
},
+ isFlowNone,
itemsPerPageOptions: [
{ value: 10, title: '10' },
{ value: 20, title: '20' },
diff --git a/src/components/cylc/tree/TreeItem.vue b/src/components/cylc/tree/TreeItem.vue
index 849f54727..da3b1fd0c 100644
--- a/src/components/cylc/tree/TreeItem.vue
+++ b/src/components/cylc/tree/TreeItem.vue
@@ -20,6 +20,7 @@ along with this program. If not, see .
v-show="!filteredOutNodesCache.get(node)"
class="c-treeitem"
:data-node-type="node.type"
+ :data-node-name="node.name"
>
.
/>
{{ node.name }}
-
+
.
/>
{{ node.name }}
-
+
+
.