Skip to content

Commit 87344a7

Browse files
authored
Merge pull request #2370 from oliver-sanders/view-toolbar++
view toolbar: add search & filter functionality
2 parents e8e2fce + 13bd1f6 commit 87344a7

File tree

26 files changed

+752
-322
lines changed

26 files changed

+752
-322
lines changed

changes.d/2370.feat.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Task filtering improvements:
2+
* Task ID filtering now supports globs.
3+
* Task state filtering now supports queued, runahead, wallclock, xtriggered, retry, held and skip selectors.
4+
* Task filtering controls have been redesigned.

src/components/cylc/TaskFilter.vue

Lines changed: 0 additions & 61 deletions
This file was deleted.

src/components/cylc/ViewToolbar.vue

Lines changed: 190 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,90 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
3030
class="control"
3131
:data-cy="`control-${iControl.key}`"
3232
>
33-
<v-btn
34-
@click="iControl.callback"
35-
v-bind="btnProps"
36-
:disabled="iControl.disabled"
37-
:aria-checked="iControl.value"
38-
:color="iControl.value ? 'blue' : undefined"
39-
role="switch"
33+
<!-- menu component (to support dropdowns) -->
34+
<v-menu
35+
eager
36+
:close-on-content-click="false"
4037
>
41-
<v-icon>{{ iControl.icon }}</v-icon>
42-
<v-tooltip>{{ iControl.title }}</v-tooltip>
43-
</v-btn>
38+
<template v-slot:activator="{ props }">
39+
<!-- inputs -->
40+
<v-text-field
41+
v-if="iControl.action === 'input'"
42+
v-model="iControl.value"
43+
class="input"
44+
v-bind="iControl.props"
45+
clearable
46+
:prepend-inner-icon="iControl.icon"
47+
@update:modelValue="iControl.callback"
48+
@focus="autoResizeInput"
49+
@blur="autoResizeInput"
50+
/>
51+
52+
<!-- buttons -->
53+
<v-btn
54+
v-else
55+
class="control-btn"
56+
v-bind="{...$attrs, ...props, ...btnProps}"
57+
@click="(e) => {iControl.action === 'menu' ? null : iControl.callback(e)}"
58+
:disabled="iControl.disabled"
59+
:aria-checked="iControl.value"
60+
:color="isSet(iControl.value) ? 'blue' : undefined"
61+
role="switch"
62+
density="compact"
63+
>
64+
<v-icon>{{ iControl.icon[iControl.value] || iControl.icon }}</v-icon>
65+
<v-tooltip>{{ iControl.title }}</v-tooltip>
66+
</v-btn>
67+
</template>
68+
69+
<!-- dropdowns -->
70+
<v-card
71+
v-if="iControl.action === 'menu'"
72+
>
73+
<v-btn
74+
:prepend-icon="$options.icons.mdiUndo"
75+
variant="plain"
76+
@click="iControl.callback([])"
77+
block
78+
spaced="end"
79+
:data-cy="`control-${iControl.key}-reset`"
80+
>
81+
Reset
82+
</v-btn>
83+
<v-divider></v-divider>
84+
85+
<v-treeview
86+
v-bind="iControl.props"
87+
v-model:activated="iControl.value"
88+
@update:activated="iControl.callback"
89+
color="blue"
90+
density="compact"
91+
style="padding-top: 0;"
92+
>
93+
<!-- task icons (for task state filters -->
94+
<template
95+
v-slot:prepend="{ item }"
96+
v-if="iControl.props['task-state-icons']"
97+
>
98+
<Task :task="item.taskProps" />
99+
</template>
100+
</v-treeview>
101+
</v-card>
102+
</v-menu>
44103
</div>
45104
</div>
46105
</div>
47106
</template>
48107

49108
<script>
50-
import { btnProps } from '@/utils/viewToolbar'
109+
import { btnProps, taskStateItems } from '@/utils/viewToolbar'
110+
import Task from '@/components/cylc/Task.vue'
111+
import {
112+
mdiFilter,
113+
mdiMagnify,
114+
mdiUndo,
115+
} from '@mdi/js'
116+
import { TaskState, WaitingStateModifierNames } from '@/model/TaskState.model'
51117
52118
export default {
53119
name: 'ViewToolbar',
@@ -56,10 +122,18 @@ export default {
56122
'setOption'
57123
],
58124
125+
components: {
126+
Task
127+
},
128+
129+
icons: {
130+
mdiUndo,
131+
},
132+
59133
props: {
60134
groups: {
61135
required: true,
62-
type: Array
136+
type: Array,
63137
/*
64138
groups: [
65139
{
@@ -70,21 +144,40 @@ export default {
70144
{
71145
// display name
72146
title: String,
147+
73148
// unique key:
74149
// * Provided with "setOption" events.
75150
// * Used by enableIf/disableIf
76151
// * Added to the control's class list for testing.
77152
key: String
153+
154+
// icon for the control:
155+
// * Either an icon.
156+
// * Or a mapping of state to an icon.
157+
// NOTE: this is autopopulated for action="taskStateFilter | taskIDFilter"
158+
icon: Icon | Object[key, Icon]
159+
78160
// action to perform when clicked:
161+
// Generic actions:
79162
// * toggle - toggle true/false
80163
// * callback - call the provided callback
164+
// * menu - open a menu (provide props: {items} in v-treeview format)
165+
// Specialised actions:
166+
// * taskIDFilter - Search box for task IDs
167+
// * taskStateFilter - open a task state filter menu
81168
action: String
169+
82170
// for use with action='callback'
83171
callback: Fuction
172+
173+
// props to be set on the control
174+
props: Object
175+
84176
// list of keys
85177
// only enable this control if all of the listed controls have
86178
// truthy values
87179
enableIf
180+
88181
// list of keys
89182
// disable this control if any of the listed controls have
90183
// truthy values
@@ -111,6 +204,8 @@ export default {
111204
let iControl
112205
let callback // callback to fire when control is activated
113206
let disabled // true if control should not be enabled
207+
let props
208+
let action
114209
const values = this.getValues()
115210
for (const group of this.groups) {
116211
iGroup = {
@@ -120,15 +215,43 @@ export default {
120215
for (const control of group.controls) {
121216
callback = null
122217
disabled = false
218+
props = control.props || {}
219+
action = control.action
123220
124221
// set callback
125-
switch (control.action) {
126-
case 'toggle':
222+
switch (action) {
223+
case 'toggle': // toggle button
127224
callback = (e) => this.toggle(control, e)
128225
break
129-
case 'callback':
226+
case 'callback': // button which actions a callback
130227
callback = (e) => this.call(control, e)
131228
break
229+
case 'taskIDFilter': // specialised "input" for filtering tasks
230+
callback = (value) => this.set(control, value)
231+
control.icon = mdiMagnify
232+
action = 'input'
233+
props = {
234+
placeholder: 'Search (globs supported)',
235+
...props,
236+
}
237+
break
238+
case 'input': // text input
239+
callback = (value) => this.set(control, value)
240+
break
241+
case 'taskStateFilter': // specialised "menu" for filtering tasks
242+
action = 'menu'
243+
control.icon = mdiFilter
244+
props = {
245+
items: taskStateItems,
246+
'indent-lines': true,
247+
activatable: true,
248+
'active-strategy': 'independent',
249+
'item-value': 'value',
250+
'task-state-icons': true, // flag to enable special slots
251+
...props,
252+
}
253+
callback = (value) => this.set(control, value)
254+
break
132255
}
133256
134257
// set disabled
@@ -147,6 +270,8 @@ export default {
147270
148271
iControl = {
149272
...control,
273+
action,
274+
props,
150275
callback,
151276
disabled
152277
}
@@ -186,6 +311,40 @@ export default {
186311
}
187312
return vars
188313
},
314+
set (control, value) {
315+
// update the value
316+
if ( // special logic for the taskStateFilter
317+
control.action === 'taskStateFilter' &&
318+
// if a waiting state modifier is selected
319+
value.some((modifier) => WaitingStateModifierNames.includes(modifier)) &&
320+
// but the waiting state is not
321+
!value.includes(TaskState.WAITING.name)
322+
) {
323+
// then add the waiting state (i.e, don't allow the user to de-select
324+
// waiting whilst a modifier is in play)
325+
value.push(TaskState.WAITING.name)
326+
}
327+
this.$emit('setOption', control.key, value)
328+
},
329+
autoResizeInput (e) {
330+
// enlarge a text input when focused or containing text
331+
if (e.type === 'focus') {
332+
e.target.classList.add('expanded')
333+
} else {
334+
if (e.target.value) {
335+
e.target.classList.add('expanded')
336+
} else {
337+
e.target.classList.remove('expanded')
338+
}
339+
}
340+
},
341+
isSet (value) {
342+
// determine if a control is active or not
343+
if (Array.isArray(value)) {
344+
return value.length
345+
}
346+
return value
347+
}
189348
}
190349
}
191350
</script>
@@ -204,11 +363,25 @@ export default {
204363
// place a divider between groups
205364
content: '';
206365
height: 70%;
207-
width: 2px;
208-
background: rgb(0, 0, 0, 0.22);
366+
width: 0.15em;
367+
border-radius: 0.15em;
368+
background: rgb(0, 0, 0, 0.18);
209369
// put a bit of space between the groups
210370
margin: 0 $spacing;
211371
}
372+
373+
// pack buttons more tightly than the vuetify default
374+
.control-btn {
375+
margin: 0.4em 0.25em 0.4em 0.25em;
376+
}
377+
378+
// auto expand/collapse the search bar
379+
.input {
380+
width: 8em;
381+
}
382+
.input:has(input.expanded) {
383+
width: 20em;
384+
}
212385
}
213386
}
214387
</style>

0 commit comments

Comments
 (0)