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"
>
-
+
.
: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 7de1256c7..d4769c6a1 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 }}
-
- {{ formatFlowNums(node.node.flowNums) }}
-
- Flows: {{ formatFlowNums(node.node.flowNums) }}
-
-
-
+
+
.
diff --git a/src/services/mock/json/workflows/multi.json b/src/services/mock/json/workflows/multi.json
index 83906f8b8..67a717f4d 100644
--- a/src/services/mock/json/workflows/multi.json
+++ b/src/services/mock/json/workflows/multi.json
@@ -145,6 +145,7 @@
"isQueued": true,
"isRunahead": false,
"name": "foo",
+ "flowNums": "[1]",
"task": {
"meanElapsedTime": 0,
"__typename": "Task"
@@ -162,6 +163,7 @@
"isQueued": false,
"isRunahead": false,
"name": "foo",
+ "flowNums": "[1]",
"task": {
"meanElapsedTime": 0,
"__typename": "Task"
diff --git a/src/services/mock/json/workflows/one.json b/src/services/mock/json/workflows/one.json
index 7ba953204..2e05e4d04 100644
--- a/src/services/mock/json/workflows/one.json
+++ b/src/services/mock/json/workflows/one.json
@@ -154,7 +154,7 @@
"isQueued": false,
"isRunahead": false,
"cyclePoint": "20000102T0000Z",
- "flowNums": "[1]",
+ "flowNums": "[1,2]",
"firstParent": {
"id": "~user/one//20000102T0000Z/BAD",
"name": "BAD",
@@ -187,12 +187,12 @@
{
"id": "~user/one//20000102T0000Z/sleepy",
"name": "sleepy",
- "state": "",
+ "state": "submitted",
"isHeld": false,
"isQueued": false,
"isRunahead": false,
"cyclePoint": "20000102T0000Z",
- "flowNums": "[1]",
+ "flowNums": "[]",
"firstParent": {
"id": "~user/one//20000102T0000Z/root",
"name": "root",
@@ -226,7 +226,7 @@
{
"id": "~user/one//20000102T0000Z/waiting",
"name": "waiting",
- "state": "",
+ "state": "waiting",
"isHeld": false,
"isQueued": false,
"isRunahead": false,
@@ -409,6 +409,20 @@
"finishedTime": "2020-11-08T22:57:19Z",
"state": "succeeded",
"submitNum": 1
+ },
+ {
+ "id": "~user/one//20000102T0000Z/sleepy/1",
+ "firstParent": {
+ "id": "~user/one//20000102T0000Z/sleepy"
+ },
+ "jobRunnerName": "background",
+ "jobId": "61983",
+ "platform": "localhost",
+ "startedTime": "",
+ "submittedTime": "2020-11-08T23:02:09Z",
+ "finishedTime": "",
+ "state": "submitted",
+ "submitNum": 1
}
],
"edges": [
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 0e3534104..d7a75a465 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -96,3 +96,9 @@ html {
.apexcharts-text {
font-size: 0.9rem;
}
+
+.c-tree, .c-table, .c-graph {
+ .flow-none {
+ opacity: 0.6;
+ }
+}
diff --git a/src/utils/tasks.js b/src/utils/tasks.js
index 53ac914a2..5c4d6f315 100644
--- a/src/utils/tasks.js
+++ b/src/utils/tasks.js
@@ -18,6 +18,13 @@
import TaskState from '@/model/TaskState.model'
import { TASK_OUTPUT_NAMES } from '@/model/TaskOutput.model'
+/**
+ * @typedef TaskNode
+ * @property {string?} flowNums
+ * @property {string} state
+ * @property {boolean} isQueued
+ */
+
/**
* States used when the parent is stopped.
* @type {TaskState[]}
@@ -131,3 +138,24 @@ export function dtMean (taskNode) {
export function formatFlowNums (flowNums) {
return JSON.parse(flowNums).join(', ') || 'None'
}
+
+/**
+ * Return whether a task has concrete flow numbers
+ * (i.e. not data store "ghost tasks").
+ *
+ * @param {TaskNode} node
+ * @returns {boolean}
+ */
+export function flowNumsValid (node) {
+ return Boolean(node.flowNums && (node.state !== 'waiting' || node.isQueued))
+}
+
+/**
+ * Return whether a task is in the None flow.
+ *
+ * @param {TaskNode} node
+ * @returns {boolean}
+ */
+export function isFlowNone (node) {
+ return Boolean(node.flowNums && !JSON.parse(node.flowNums).length)
+}
diff --git a/src/views/Graph.vue b/src/views/Graph.vue
index 2c149952c..b3ab51d13 100644
--- a/src/views/Graph.vue
+++ b/src/views/Graph.vue
@@ -59,6 +59,7 @@ along with this program. If not, see .
:task="node"
:jobs="node.children"
:jobTheme="jobTheme"
+ :class="{ 'flow-none': isFlowNone(node.node) }"
/>