Skip to content

Commit ecf94bf

Browse files
authored
Merge pull request #1187 from MetRonnie/gscan-filter
Improve GScan filter menu
2 parents 1bec0e4 + 12714de commit ecf94bf

File tree

4 files changed

+230
-228
lines changed

4 files changed

+230
-228
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ creating a new release entry be sure to copy & paste the span tag with the
1010
`actions:bind` attribute, which is used by a regex to find the text to be
1111
updated. Only the first match gets replaced, so it's fine to leave the old
1212
ones in. -->
13+
-------------------------------------------------------------------------------
14+
## __cylc-ui-1.6.0 (<span actions:bind='release-date'>Upcoming</span>)__
15+
16+
### Enhancements
17+
18+
[#1187](https://github.com/cylc/cylc-ui/pull/1187) - Improved the workflow
19+
filtering menu in the sidebar.
20+
1321
-------------------------------------------------------------------------------
1422
## __cylc-ui-1.5.0 (<span actions:bind='release-date'>Released 2023-02-20</span>)__
1523

src/components/cylc/gscan/GScan.vue

Lines changed: 71 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -38,64 +38,55 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
3838
id="c-gscan-search-workflows"
3939
/>
4040
<v-menu
41-
v-model="showFilterTooltip"
41+
v-model="showFilterMenu"
4242
:close-on-content-click="false"
4343
offset-x
4444
>
45-
<!-- button to activate the filters tooltip -->
45+
<!-- button to activate the filters menu -->
4646
<template v-slot:activator="{ on, attrs }">
47-
<v-btn
48-
v-bind="attrs"
49-
v-on="on"
50-
link
51-
icon
52-
class="flex-grow-0 flex-column"
53-
id="c-gscan-filter-tooltip-btn"
54-
@click="showFilterTooltip = !showFilterTooltip"
47+
<v-badge
48+
:content="numFilters"
49+
:value="numFilters"
50+
overlap
5551
>
56-
<v-icon>{{ $options.icons.mdiFilter }}</v-icon>
57-
</v-btn>
52+
<v-btn
53+
v-bind="attrs"
54+
v-on="on"
55+
link
56+
icon
57+
id="c-gscan-filter-menu-btn"
58+
@click="showFilterMenu = !showFilterMenu"
59+
>
60+
<v-icon>{{ $options.icons.mdiFilter }}</v-icon>
61+
</v-btn>
62+
</v-badge>
5863
</template>
59-
<!-- filters tooltip -->
60-
<v-card
61-
max-height="250px"
62-
>
63-
<v-list
64-
dense
65-
>
64+
<!-- filters menu -->
65+
<v-card width="500px">
66+
<v-list dense>
6667
<div
67-
v-for="filter in filters"
68-
:key="filter.title"
68+
v-for="(items, title) in filters"
69+
:key="title"
6970
>
70-
<v-list-item dense>
71+
<v-list-item>
7172
<v-list-item-content ma-0>
72-
<v-list-item-title>
73-
<v-checkbox
74-
:label="filter.title"
75-
:input-value="allItemsSelected(filter.items)"
76-
value
77-
dense
78-
hide-details
79-
hint="Toggle all"
80-
@click="toggleItemsValues(filter.items)"
81-
></v-checkbox>
82-
</v-list-item-title>
83-
</v-list-item-content>
84-
</v-list-item>
85-
<v-divider />
86-
<v-list-item
87-
v-for="item in filter.items"
88-
:key="`${filter.title}-${item.text}`"
89-
dense
90-
>
91-
<v-list-item-action>
92-
<v-checkbox
93-
:label="item.text"
94-
v-model="item.model"
73+
<span class="mb-2">Filter by {{ title }}</span>
74+
<v-autocomplete
75+
v-model="filters[title]"
76+
:items="$options.allStates[title]"
77+
outlined
78+
chips
79+
deletable-chips
80+
small-chips
81+
clearable
82+
multiple
9583
dense
96-
height="20"
97-
></v-checkbox>
98-
</v-list-item-action>
84+
hide-details
85+
:menu-props="{ closeOnClick: true }"
86+
:clear-icon="$options.icons.mdiClose"
87+
:data-cy="`filter ${title}`"
88+
/>
89+
</v-list-item-content>
9990
</v-list-item>
10091
</div>
10192
</v-list>
@@ -202,8 +193,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
202193

203194
<script>
204195
import { mdiClose, mdiFilter } from '@mdi/js'
205-
import uniq from 'lodash/uniq'
206-
import TaskState from '@/model/TaskState.model'
196+
import TaskState, { TaskStateUserOrder } from '@/model/TaskState.model'
207197
import { WorkflowState } from '@/model/WorkflowState.model'
208198
import Job from '@/components/cylc/Job'
209199
import Tree from '@/components/cylc/tree/Tree'
@@ -246,68 +236,20 @@ export default {
246236
*/
247237
searchWorkflows: '',
248238
/**
249-
* Variable to control whether the filters tooltip (a menu actually)
250-
* is displayed or not (i.e. v-model=this).
239+
* Whether the filters menu is displayed (i.e. v-model=this).
251240
* @type {boolean}
252241
*/
253-
showFilterTooltip: false,
242+
showFilterMenu: false,
254243
/**
255-
* List of filters to be displayed by the template, so that the user
256-
* can filter the list of workflows.
257-
*
258-
* Each entry contains a title and a list of items. Each item contains
259-
* the text attribute, which is used as display value in the template.
260-
* The value attribute, which may be used if necessary, as it contains
261-
* the original value (e.g. an Enum, while title would be some formatted
262-
* string). Finally, the model is bound via v-model, and keeps the
263-
* value selected in the UI (i.e. if the user checks the "running"
264-
* checkbox, the model here will be true, if the user unchecks it,
265-
* then it will be false).
266-
*
267-
* @type {[
268-
* {
269-
* title: string,
270-
* items: [{
271-
* text: string,
272-
* value: object,
273-
* model: boolean
274-
* }]
275-
* }
276-
* ]}
244+
* List of filters selected by the user.
245+
* @type {{string: string[]}}
277246
*/
278-
filters: [
279-
{
280-
title: 'workflow state',
281-
items: WorkflowState.enumValues
282-
.map(state => {
283-
return {
284-
text: state.name,
285-
value: state,
286-
model: true
287-
}
288-
})
289-
},
290-
{
291-
title: 'task state',
292-
items: TaskState.enumValues.map(state => {
293-
return {
294-
text: state.name,
295-
value: state,
296-
model: false
297-
}
298-
}).sort((left, right) => {
299-
return left.text.localeCompare(right.text)
300-
})
301-
}
302-
// {
303-
// title: 'workflow host',
304-
// items: [] // TODO: will it be in state totals?
305-
// },
306-
// {
307-
// title: 'cylc version',
308-
// items: [] // TODO: will it be in state totals?
309-
// }
310-
]
247+
filters: {
248+
'workflow state': [],
249+
'task state': []
250+
// 'workflow host': [], // TODO: will it be in state totals?
251+
// 'cylc version': [] // TODO: will it be in state totals?
252+
}
311253
}
312254
},
313255
computed: {
@@ -318,23 +260,8 @@ export default {
318260
}
319261
return sortedWorkflowTree(this.workflowTree)
320262
},
321-
/**
322-
* @return {Array<String>}
323-
*/
324-
workflowStates () {
325-
return this.filters[0]
326-
.items
327-
.filter(item => item.model)
328-
.map(item => item.value.name)
329-
},
330-
/**
331-
* @return {Array<String>}
332-
*/
333-
taskStates () {
334-
return uniq(this.filters[1]
335-
.items
336-
.filter(item => item.model)
337-
.map(item => item.value.name))
263+
numFilters () {
264+
return Object.values(this.filters).flat().length
338265
}
339266
},
340267
watch: {
@@ -345,25 +272,19 @@ export default {
345272
filters: {
346273
deep: true,
347274
immediate: false,
348-
handler: function (newVal) {
349-
this.filteredWorkflows = this.filterHierarchically(this.workflows, this.searchWorkflows, this.workflowStates, this.taskStates)
350-
}
275+
handler: 'filterWorkflows'
351276
},
352277
/**
353278
* If the user changes the workflow name to search/filter,
354279
* then we apply the filters to the list of workflows.
355280
*/
356281
searchWorkflows: {
357282
immediate: false,
358-
handler: function (newVal) {
359-
this.filteredWorkflows = this.filterHierarchically(this.workflows, newVal, this.workflowStates, this.taskStates)
360-
}
283+
handler: 'filterWorkflows'
361284
},
362285
workflows: {
363286
immediate: true,
364-
handler: function () {
365-
this.filteredWorkflows = this.filterHierarchically(this.workflows, this.searchWorkflows, this.workflowStates, this.taskStates)
366-
}
287+
handler: 'filterWorkflows'
367288
},
368289
filteredWorkflows: {
369290
immediate: true,
@@ -394,7 +315,14 @@ export default {
394315
}
395316
},
396317
methods: {
397-
filterHierarchically,
318+
filterWorkflows () {
319+
this.filteredWorkflows = filterHierarchically(
320+
this.workflows,
321+
this.searchWorkflows,
322+
this.filters['workflow state'],
323+
this.filters['task state']
324+
)
325+
},
398326
399327
/**
400328
* Return `true` iff all the items have been selected. `false` otherwise.
@@ -474,6 +402,14 @@ export default {
474402
icons: {
475403
mdiClose,
476404
mdiFilter
405+
},
406+
/**
407+
* Lists of all the possible workflow and task states
408+
* @type {{string: string[]}}
409+
*/
410+
allStates: {
411+
'workflow state': WorkflowState.enumValues.map(x => x.name),
412+
'task state': TaskStateUserOrder.map(x => x.name)
477413
}
478414
}
479415
</script>

src/components/cylc/gscan/filters.js

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,42 +28,40 @@ export function filterByName (workflow, name) {
2828

2929
/**
3030
* @private
31-
* @param stateTotals {Object} - object with the keys being states, and values the count
32-
* @return {Array<String>}
31+
* @param {Object=} stateTotals - object with the keys being states, and values the count
32+
* @return {string[]}
3333
*/
3434
function getWorkflowStates (stateTotals) {
3535
const jobStates = JobState.enumValues.map(jobState => jobState.name)
36+
// GraphQL will return all the task states possible in a workflow, but we
37+
// only want the states that have an equivalent state for a job. So we filter
38+
// out the states that do not exist for jobs, and that have active tasks in
39+
// the workflow (no point keeping the empty states, as they are not to be
40+
// displayed).
3641
return !stateTotals
3742
? []
38-
: Object
39-
.entries(stateTotals)
40-
.filter(stateTotal => {
41-
// GraphQL will return all the task states possible in a workflow, but we
42-
// only want the states that have an equivalent state for a job. So we filter
43-
// out the states that do not exist for jobs, and that have active tasks in
44-
// the workflow (no point keeping the empty states, as they are not to be
45-
// displayed).
46-
return jobStates.includes(stateTotal[0]) && stateTotal[1] > 0
47-
})
48-
.map(stateTotal => stateTotal[0])
43+
: Object.keys(stateTotals)
44+
.filter((state) => jobStates.includes(state) && stateTotals[state] > 0)
4945
}
5046

5147
/**
5248
* @param {WorkflowGScanNode|WorkflowNamePartGScanNode} workflow
53-
* @param {Array<String>} workflowStates
54-
* @param {Array<String>} taskStates
49+
* @param {string[]} workflowStates
50+
* @param {string[]} taskStates
5551
* @returns {boolean}
5652
*/
5753
export function filterByState (workflow, workflowStates, taskStates) {
5854
// workflow states
59-
if (!workflowStates.includes(workflow.node.status)) {
55+
if (
56+
workflowStates.length && !workflowStates.includes(workflow.node.status)
57+
) {
6058
return false
6159
}
6260
// task states
63-
if (taskStates.length > 0) {
64-
const intersection = getWorkflowStates(workflow.node.stateTotals)
65-
.filter(item => taskStates.includes(item))
66-
return intersection.length !== 0
61+
if (taskStates.length) {
62+
return getWorkflowStates(workflow.node.stateTotals).some(
63+
(item) => taskStates.includes(item)
64+
)
6765
}
6866
return true
6967
}
@@ -78,24 +76,19 @@ export function filterByState (workflow, workflowStates, taskStates) {
7876
*
7977
* @param {WorkflowGScanNode|WorkflowNamePartGScanNode} workflow
8078
* @param {string} name
81-
* @param {Array<String>} workflowStates
82-
* @param {Array<String>} taskStates
79+
* @param {string[]} workflowStates
80+
* @param {string[]} taskStates
8381
* @return {boolean} - true if the workflow is accepted, false otherwise
8482
*/
8583
function filterWorkflow (workflow, name, workflowStates, taskStates) {
86-
let filtered = false
8784
// Filter by name.
88-
if (name && name !== '') {
89-
filtered = filterByName(workflow, name)
85+
if (name && !filterByName(workflow, name)) {
9086
// Stop if we know that the name was not accepted.
91-
if (!filtered) {
92-
return filtered
93-
}
87+
return false
9488
}
9589
// Now filter using the provided list of states. We know that the name has been
9690
// accepted at this point.
97-
filtered = filterByState(workflow, workflowStates, taskStates)
98-
return filtered
91+
return filterByState(workflow, workflowStates, taskStates)
9992
}
10093

10194
/**

0 commit comments

Comments
 (0)