Skip to content

Commit 2a7c98a

Browse files
committed
Improve GScan filter menu
1 parent aab52b3 commit 2a7c98a

File tree

3 files changed

+229
-232
lines changed

3 files changed

+229
-232
lines changed

src/components/cylc/gscan/GScan.vue

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

202193
<script>
203-
import { mdiFilter } from '@mdi/js'
204-
import uniq from 'lodash/uniq'
205-
import TaskState from '@/model/TaskState.model'
194+
import { mdiClose, mdiFilter } from '@mdi/js'
195+
import TaskState, { TaskStateUserOrder } from '@/model/TaskState.model'
206196
import { WorkflowState } from '@/model/WorkflowState.model'
207197
import Job from '@/components/cylc/Job'
208198
import Tree from '@/components/cylc/tree/Tree'
@@ -233,9 +223,6 @@ export default {
233223
data () {
234224
return {
235225
maximumTasksDisplayed: 5,
236-
svgPaths: {
237-
filter: mdiFilter
238-
},
239226
/**
240227
* The filtered workflows. This is the result of applying the filters
241228
* on the workflows prop.
@@ -248,68 +235,20 @@ export default {
248235
*/
249236
searchWorkflows: '',
250237
/**
251-
* Variable to control whether the filters tooltip (a menu actually)
252-
* is displayed or not (i.e. v-model=this).
238+
* Whether the filters menu is displayed (i.e. v-model=this).
253239
* @type {boolean}
254240
*/
255-
showFilterTooltip: false,
241+
showFilterMenu: false,
256242
/**
257-
* List of filters to be displayed by the template, so that the user
258-
* can filter the list of workflows.
259-
*
260-
* Each entry contains a title and a list of items. Each item contains
261-
* the text attribute, which is used as display value in the template.
262-
* The value attribute, which may be used if necessary, as it contains
263-
* the original value (e.g. an Enum, while title would be some formatted
264-
* string). Finally, the model is bound via v-model, and keeps the
265-
* value selected in the UI (i.e. if the user checks the "running"
266-
* checkbox, the model here will be true, if the user unchecks it,
267-
* then it will be false).
268-
*
269-
* @type {[
270-
* {
271-
* title: string,
272-
* items: [{
273-
* text: string,
274-
* value: object,
275-
* model: boolean
276-
* }]
277-
* }
278-
* ]}
243+
* List of filters selected by the user.
244+
* @type {{string: string[]}}
279245
*/
280-
filters: [
281-
{
282-
title: 'workflow state',
283-
items: WorkflowState.enumValues
284-
.map(state => {
285-
return {
286-
text: state.name,
287-
value: state,
288-
model: true
289-
}
290-
})
291-
},
292-
{
293-
title: 'task state',
294-
items: TaskState.enumValues.map(state => {
295-
return {
296-
text: state.name,
297-
value: state,
298-
model: false
299-
}
300-
}).sort((left, right) => {
301-
return left.text.localeCompare(right.text)
302-
})
303-
}
304-
// {
305-
// title: 'workflow host',
306-
// items: [] // TODO: will it be in state totals?
307-
// },
308-
// {
309-
// title: 'cylc version',
310-
// items: [] // TODO: will it be in state totals?
311-
// }
312-
]
246+
filters: {
247+
'workflow state': [],
248+
'task state': []
249+
// 'workflow host': [], // TODO: will it be in state totals?
250+
// 'cylc version': [] // TODO: will it be in state totals?
251+
}
313252
}
314253
},
315254
computed: {
@@ -320,23 +259,8 @@ export default {
320259
}
321260
return sortedWorkflowTree(this.workflowTree)
322261
},
323-
/**
324-
* @return {Array<String>}
325-
*/
326-
workflowStates () {
327-
return this.filters[0]
328-
.items
329-
.filter(item => item.model)
330-
.map(item => item.value.name)
331-
},
332-
/**
333-
* @return {Array<String>}
334-
*/
335-
taskStates () {
336-
return uniq(this.filters[1]
337-
.items
338-
.filter(item => item.model)
339-
.map(item => item.value.name))
262+
numFilters () {
263+
return Object.values(this.filters).flat().length
340264
}
341265
},
342266
watch: {
@@ -347,25 +271,19 @@ export default {
347271
filters: {
348272
deep: true,
349273
immediate: false,
350-
handler: function (newVal) {
351-
this.filteredWorkflows = this.filterHierarchically(this.workflows, this.searchWorkflows, this.workflowStates, this.taskStates)
352-
}
274+
handler: 'filterWorkflows'
353275
},
354276
/**
355277
* If the user changes the workflow name to search/filter,
356278
* then we apply the filters to the list of workflows.
357279
*/
358280
searchWorkflows: {
359281
immediate: false,
360-
handler: function (newVal) {
361-
this.filteredWorkflows = this.filterHierarchically(this.workflows, newVal, this.workflowStates, this.taskStates)
362-
}
282+
handler: 'filterWorkflows'
363283
},
364284
workflows: {
365285
immediate: true,
366-
handler: function () {
367-
this.filteredWorkflows = this.filterHierarchically(this.workflows, this.searchWorkflows, this.workflowStates, this.taskStates)
368-
}
286+
handler: 'filterWorkflows'
369287
},
370288
filteredWorkflows: {
371289
immediate: true,
@@ -396,7 +314,14 @@ export default {
396314
}
397315
},
398316
methods: {
399-
filterHierarchically,
317+
filterWorkflows () {
318+
this.filteredWorkflows = filterHierarchically(
319+
this.workflows,
320+
this.searchWorkflows,
321+
this.filters['workflow state'],
322+
this.filters['task state']
323+
)
324+
},
400325
401326
/**
402327
* Return `true` iff all the items have been selected. `false` otherwise.
@@ -470,6 +395,20 @@ export default {
470395
return validValues.includes(entry[0])
471396
})
472397
}
398+
},
399+
400+
// Misc options
401+
icons: {
402+
mdiClose,
403+
mdiFilter
404+
},
405+
/**
406+
* Lists of all the possible workflow and task states
407+
* @type {{string: string[]}}
408+
*/
409+
allStates: {
410+
'workflow state': WorkflowState.enumValues.map(x => x.name),
411+
'task state': TaskStateUserOrder.map(x => x.name)
473412
}
474413
}
475414
</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)