Skip to content

Commit 6270e7f

Browse files
committed
Reimplement latest tasks tooltips & simplify
1 parent fe2b566 commit 6270e7f

File tree

4 files changed

+102
-109
lines changed

4 files changed

+102
-109
lines changed

src/components/cylc/TaskStateBadge.vue

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,29 @@
1919
<div
2020
class="task-state-badge d-flex justify-center align-center px-1 font-weight-medium"
2121
:class="state"
22-
v-tooltip="{ text: tooltip, openDelay: 400, location: 'top' }"
2322
>
2423
{{ value }}
24+
<v-tooltip
25+
location="top"
26+
:open-delay="400"
27+
>
28+
{{ value }} {{ state }} task{{ value > 1 ? 's': '' }}.
29+
<template v-if="latestTasks.length">
30+
Latest:
31+
<span
32+
v-for="(task, index) in latestTasks.slice(0, maxLatestTasks)"
33+
:key="index"
34+
class="text-grey-lighten-1"
35+
>
36+
<br/>{{ task }}
37+
</span>
38+
</template>
39+
</v-tooltip>
2540
</div>
2641
</template>
2742

2843
<script setup>
29-
import { computed } from 'vue'
30-
31-
const props = defineProps({
44+
defineProps({
3245
state: {
3346
type: String,
3447
required: true
@@ -37,9 +50,13 @@ const props = defineProps({
3750
type: Number,
3851
required: true,
3952
},
53+
latestTasks: {
54+
type: Array,
55+
default: () => [],
56+
},
57+
maxLatestTasks: {
58+
type: Number,
59+
default: 5,
60+
},
4061
})
41-
42-
const tooltip = computed(
43-
() => `${props.value} ${props.state} task${props.value > 1 ? 's' : ''}`
44-
)
4562
</script>

src/components/cylc/tree/GScanTreeItem.vue

Lines changed: 69 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
<template>
1919
<TreeItem
2020
v-bind="{ node, depth, filteredOutNodesCache, hoverable }"
21-
:auto-expand-types="$options.nodeTypes"
21+
:auto-expand-types="nodeTypes"
2222
:render-expand-collapse-btn="node.type !== 'workflow'"
2323
ref="treeItem"
2424
>
@@ -47,16 +47,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
4747
</span>
4848
</div>
4949
<div class="d-flex c-gscan-workflow-states flex-grow-0">
50-
<template
51-
v-for="state in Object.keys(descendantTaskInfo.latestTasks)"
52-
:key="`${node.id}-${state}`"
53-
>
54-
<TaskStateBadge
55-
v-if="descendantTaskInfo.stateTotals[state]"
56-
:state="state"
57-
:value="descendantTaskInfo.stateTotals[state]"
58-
/>
59-
</template>
50+
<TaskStateBadge
51+
v-for="(value, state) in statesInfo.stateTotals"
52+
:key="state"
53+
v-bind="{ state, value }"
54+
:latest-tasks="statesInfo.latestTasks[state]"
55+
/>
6056
<WarningIcon
6157
v-if="workflowWarnings"
6258
:workflow="node"
@@ -80,117 +76,97 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
8076
</TreeItem>
8177
</template>
8278

83-
<script>
79+
<script setup>
80+
import { computed } from 'vue'
8481
import TaskStateBadge from '@/components/cylc/TaskStateBadge.vue'
8582
import WorkflowIcon from '@/components/cylc/gscan/WorkflowIcon.vue'
8683
import TreeItem from '@/components/cylc/tree/TreeItem.vue'
8784
import WarningIcon from '@/components/cylc/WarningIcon.vue'
88-
import { JobStateNames } from '@/model/JobState.model'
85+
import TaskState from '@/model/TaskState.model'
8986
import { WorkflowState } from '@/model/WorkflowState.model'
9087
import { useWorkflowWarnings } from '@/composables/localStorage'
9188
89+
const nodeTypes = ['workflow-part', 'workflow']
90+
91+
/** Display order in sidebar */
92+
const taskStatesOrdered = [
93+
TaskState.FAILED.name,
94+
TaskState.SUBMIT_FAILED.name,
95+
TaskState.SUBMITTED.name,
96+
TaskState.RUNNING.name,
97+
]
98+
9299
/**
93-
* Get aggregated task state totals and latest task states for all descendents of a node.
100+
* Get aggregated task state totals for all descendents of a node.
94101
*
95102
* @param {Object} node
96-
* @param {Record<string, number>} stateTotals
97-
* @param {Record<string, string[]>} latestTasks
98-
* @param {Boolean} topLevel - true if the traversal depth is 0, else false.
103+
* @param {Record<string, number>} stateTotals - Accumulator for state totals.
104+
* @param {Record<string, string[]>} latestTasks - Accumulator for latest tasks.
99105
*/
100-
function traverseChildren (node, stateTotals = {}, latestTasks = {}, topLevel = true) {
106+
function getStatesInfo (node, stateTotals = {}, latestTasks = {}) {
101107
// if we aren't at the end of the node tree, continue recurse until we hit something other then a workflow part
102108
if (node.type === 'workflow-part' && node.children) {
103-
// at every branch, recurse all child nodes
109+
// at every branch, recurse all child nodes except stopped workflows
104110
for (const child of node.children) {
105-
traverseChildren(child, stateTotals, latestTasks, false)
111+
if (child.node.status !== WorkflowState.STOPPED.name) {
112+
getStatesInfo(child, stateTotals, latestTasks)
113+
}
106114
}
107115
} else if (node.type === 'workflow' && node.node.stateTotals) {
108-
// if we are at the end of a node (or at least, hit a workflow node), stop and merge state
109-
110-
// the latest state tasks from this node with all the others from the tree
111-
for (const [state, totals] of Object.entries(node.node.stateTotals)) {
112-
if (
113-
// filter only valid states
114-
JobStateNames.includes(state) &&
115-
// omit state totals from stopped workflows
116-
(topLevel || node.node.status !== 'stopped')
117-
) {
118-
// (cast as numbers so they dont get concatenated as strings)
119-
stateTotals[state] = (stateTotals[state] ?? 0) + parseInt(totals)
116+
// if we hit a workflow node, stop and merge state
117+
118+
// the non-zero state totals from this node with all the others from the tree
119+
for (const state of taskStatesOrdered) {
120+
const nodeTotal = node.node.stateTotals[state]
121+
if (nodeTotal) {
122+
stateTotals[state] = (stateTotals[state] ?? 0) + nodeTotal
120123
}
121-
}
122-
for (const [state, taskNames] of Object.entries(node.node.latestStateTasks)) {
123-
if (JobStateNames.includes(state)) {
124-
// concat the new tasks in where they don't already exist
124+
const nodeLatestTasks = node.node.latestStateTasks?.[state]
125+
if (nodeLatestTasks?.length) {
125126
latestTasks[state] = [
126127
...(latestTasks[state] ?? []),
127-
...taskNames,
128+
...nodeLatestTasks,
128129
].sort().reverse() // cycle point descending order
129130
}
130131
}
131132
}
132133
return { stateTotals, latestTasks }
133134
}
134135
135-
export default {
136-
name: 'GScanTreeItem',
136+
const workflowWarnings = useWorkflowWarnings()
137137
138-
components: {
139-
TaskStateBadge,
140-
TreeItem,
141-
WarningIcon,
142-
WorkflowIcon,
138+
const props = defineProps({
139+
node: {
140+
type: Object,
141+
required: true
143142
},
144-
145-
data: () => ({
146-
workflowWarnings: useWorkflowWarnings()
147-
}),
148-
149-
props: {
150-
node: {
151-
type: Object,
152-
required: true
153-
},
154-
depth: {
155-
type: Number,
156-
default: 0
157-
},
158-
filteredOutNodesCache: {
159-
type: WeakMap,
160-
required: true,
161-
},
162-
hoverable: {
163-
type: Boolean,
164-
},
143+
depth: {
144+
type: Number,
145+
default: 0
165146
},
166-
167-
computed: {
168-
workflowLink () {
169-
return this.node.type === 'workflow'
170-
? `/workspace/${ this.node.tokens.workflow }`
171-
: ''
172-
},
173-
174-
/** Task state totals and latest states for all descendents of this node. */
175-
descendantTaskInfo () {
176-
return traverseChildren(this.node)
177-
},
178-
179-
nodeChildren () {
180-
return this.node.type === 'workflow'
181-
? []
182-
: this.node.children
183-
},
184-
185-
nodeClass () {
186-
return {
187-
'c-workflow-stopped': this.node.node?.status === WorkflowState.STOPPED.name,
188-
}
189-
}
147+
filteredOutNodesCache: {
148+
type: WeakMap,
149+
required: true,
190150
},
151+
hoverable: {
152+
type: Boolean,
153+
},
154+
})
191155
192-
nodeTypes: ['workflow-part', 'workflow'],
193-
maxTasksDisplayed: 5,
194-
WorkflowState,
195-
}
156+
const workflowLink = computed(
157+
() => props.node.type === 'workflow'
158+
? `/workspace/${ props.node.tokens.workflow }`
159+
: ''
160+
)
161+
162+
/** Task state totals for all descendents of this node. */
163+
const statesInfo = computed(() => getStatesInfo(props.node))
164+
165+
const nodeChildren = computed(
166+
() => props.node.type === 'workflow' ? [] : props.node.children
167+
)
168+
169+
const nodeClass = computed(() => ({
170+
'c-workflow-stopped': props.node.node?.status === WorkflowState.STOPPED.name,
171+
}))
196172
</script>

src/services/mock/json/workflows/one.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@
2323
"latestStateTasks": {
2424
"submitted": [],
2525
"running": [
26-
"checkpoint"
26+
"20000102T0000Z/checkpoint"
2727
],
2828
"succeeded": [
29-
"eventually_succeeded",
30-
"succeeded"
29+
"20000102T0000Z/eventually_succeeded",
30+
"20000102T0000Z/succeeded"
3131
],
3232
"failed": [
33-
"failed"
33+
"20000102T0000Z/failed"
3434
]
3535
}
3636
},

tests/unit/components/cylc/tree/treeitem.vue.spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ describe('GScanTreeItem', () => {
152152
}
153153
})
154154
it('combines all descendant tasks', () => {
155-
expect(wrapper.vm.descendantTaskInfo.latestTasks.submitted.length).to.equal(10)
156-
expect(wrapper.vm.descendantTaskInfo.latestTasks.running.length).to.equal(10)
155+
expect(wrapper.vm.statesInfo.latestTasks.submitted.length).to.equal(10)
156+
expect(wrapper.vm.statesInfo.latestTasks.running.length).to.equal(10)
157157
})
158158
it('combines all descendant task totals', () => {
159-
expect(wrapper.vm.descendantTaskInfo.stateTotals.submitted).to.equal(5)
160-
expect(wrapper.vm.descendantTaskInfo.stateTotals.running).to.equal(12)
159+
expect(wrapper.vm.statesInfo.stateTotals.submitted).to.equal(5)
160+
expect(wrapper.vm.statesInfo.stateTotals.running).to.equal(12)
161161
})
162162
it('collapses to the lowest only-child', () => {
163163
expect(wrapper.vm.node.id).to.equal('~cylc/double/mid')

0 commit comments

Comments
 (0)