Skip to content

Commit

Permalink
Centralise task filtering into own component; list task states in log…
Browse files Browse the repository at this point in the history
…ical order (#1123)

* Use logical order for task state filter dropdown

Simplify filtering code

* Centralise common Tree/Table view filtering logic

* Move task filter controls into own component

* Fix bug when expanding/collapsing tree view while filters are active
  • Loading branch information
MetRonnie authored Jan 4, 2023
1 parent 12d2134 commit 09c3827
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 400 deletions.
106 changes: 106 additions & 0 deletions src/components/cylc/TaskFilter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<!--
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 <http://www.gnu.org/licenses/>.
-->

<!-- Controls for filtering tasks in views. -->

<template>
<v-row no-gutters>
<v-col
cols="12"
md="6"
class="pr-md-2 mb-2 mb-md-0"
>
<v-text-field
data-cy="filter-task-name"
clearable
dense
flat
hide-details
outlined
placeholder="Filter by task name"
v-model="localValue.name"
ref="filterNameInput"
></v-text-field>
</v-col>
<v-col
cols="12"
md="6"
class="mb-2 mb-md-0"
>
<v-select
data-cy="filter-task-states"
:items="allStates"
clearable
dense
flat
hide-details
multiple
outlined
placeholder="Filter by task state"
v-model="localValue.states"
>
<template v-slot:item="slotProps">
<Task :task="{ state: slotProps.item }" />
<span class="ml-2">{{ slotProps.item }}</span>
</template>
<template v-slot:selection="slotProps">
<div class="mr-2" v-if="slotProps.index >= 0 && slotProps.index < maxVisibleStates">
<Task :task="{ state: slotProps.item }" />
</div>
<span
v-if="slotProps.index === maxVisibleStates"
class="grey--text caption"
>
(+{{ localValue.states.length - maxVisibleStates }})
</span>
</template>
</v-select>
</v-col>
</v-row>
</template>

<script>
import Task from '@/components/cylc/Task'
import { TaskStateUserOrder } from '@/model/TaskState.model'
export default {
name: 'TaskFilter',
components: {
Task
},
props: {
value: Object // { name, states }
},
data () {
return {
maxVisibleStates: 4,
allStates: TaskStateUserOrder.map(ts => ts.name)
}
},
computed: {
localValue: {
get () {
return this.value
},
set (value) {
// Update 'value' prop by notifying parent component's v-model for this component
this.$emit('input', value)
}
}
}
}
</script>
38 changes: 38 additions & 0 deletions src/components/cylc/common/filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/

/* Logic for filtering tasks. */

/**
* Return true if a node has matches the specified name/state filter.
*
* @export
* @param {{ name: string, state: string }} node
* @param {?string} name
* @param {?string[]} states
* @return {boolean}
*/
export function matchNode (node, name, states) {
let ret = true
if (name?.trim()) {
ret &&= node.name.includes(name)
}
if (states?.length) {
ret &&= states.includes(node.state)
}
return ret
}
4 changes: 2 additions & 2 deletions src/components/cylc/common/sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
* Declare function used in sortedIndexBy as a comparator.
*
* @callback SortedIndexByComparator
* @param {object} leftObject - left parameter object
* @param {Object} leftObject - left parameter object
* @param {string} leftValue - left parameter value
* @param {object} rightObject - right parameter object
* @param {Object} rightObject - right parameter object
* @param {string} rightValue - right parameter value
* @returns {boolean} - true if leftValue is higher than rightValue
*/
Expand Down
132 changes: 7 additions & 125 deletions src/components/cylc/table/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,62 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="filterable"
class=""
>
<v-row class="no-gutters">
<v-col
cols="12"
md="6"
class="pr-md-2 mb-2 mb-md-0"
>
<v-text-field
id="c-table-filter-task-name"
clearable
dense
flat
hide-details
outlined
placeholder="Filter by task name"
v-model.trim="tasksFilter.name"
@keyup="filterTasks"
@click:clear="clearInput"
ref="filterNameInput"
></v-text-field>
</v-col>
<v-col
cols="12"
md="6"
class="mb-2 mb-md-0"
>
<v-select
id="c-table-filter-task-states"
:items="taskStates"
clearable
dense
flat
hide-details
multiple
outlined
placeholder="Filter by task state"
v-model="tasksFilter.states"
@change="filterTasks"
>
<template v-slot:item="slotProps">
<Task :task="{ state: slotProps.item.value }" />
<span class="ml-2">{{ slotProps.item.value }}</span>
</template>
<template v-slot:selection="slotProps">
<div class="mr-2" v-if="slotProps.index >= 0 && slotProps.index < maximumTasks">
<Task :task="{ state: slotProps.item.value }" />
</div>
<span
v-if="slotProps.index === maximumTasks"
class="grey--text caption"
>
(+{{ tasksFilter.states.length - maximumTasks }})
</span>
</template>
</v-select>
</v-col>
</v-row>
<TaskFilter v-model="tasksFilter"/>
</v-col>
</v-row>
<v-row
Expand Down Expand Up @@ -217,13 +162,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</template>

<script>
import TaskState from '@/model/TaskState.model'
import Task from '@/components/cylc/Task'
import Job from '@/components/cylc/Job'
import cloneDeep from 'lodash/cloneDeep'
import { mdiChevronDown, mdiArrowDown } from '@mdi/js'
import { DEFAULT_COMPARATOR } from '@/components/cylc/common/sort'
import { datetimeComparator } from '@/components/cylc/table/sort'
import { matchNode } from '@/components/cylc/common/filter'
import TaskFilter from '@/components/cylc/TaskFilter.vue'
export default {
name: 'TableComponent',
Expand All @@ -239,7 +184,8 @@ export default {
},
components: {
Task,
Job
Job,
TaskFilter
},
data () {
return {
Expand Down Expand Up @@ -302,76 +248,12 @@ export default {
sort: (a, b) => parseInt(a ?? 0) - parseInt(b ?? 0)
}
],
tasksFilter: {
name: '',
states: []
},
activeFilters: null,
maximumTasks: 4
tasksFilter: {}
}
},
computed: {
taskStates () {
return TaskState.enumValues.map(taskState => {
return {
text: taskState.name.replace(/_/g, ' '),
value: taskState.name
}
}).sort((left, right) => {
return left.text.localeCompare(right.text)
})
},
tasksFilterStates () {
return this.activeFilters.states
},
filteredTasks () {
const filterByName = this.filterByTaskName()
const filterByState = this.filterByTaskState()
return this.tasks.filter(task => {
if (filterByName && filterByState) {
return (
task.task.name.includes(this.activeFilters.name) &&
this.tasksFilterStates.includes(task.task.node.state)
)
} else if (filterByName) {
return task.task.name.includes(this.activeFilters.name)
} else if (filterByState) {
return this.tasksFilterStates.includes(task.task.node.state)
}
return true
})
}
},
methods: {
filterByTaskName () {
return this.activeFilters &&
this.activeFilters.name !== undefined &&
this.activeFilters.name !== null &&
this.activeFilters.name !== ''
},
filterByTaskState () {
return this.activeFilters &&
this.activeFilters.states !== undefined &&
this.activeFilters.states !== null &&
this.activeFilters.states.length > 0
},
filterTasks () {
const taskNameFilterSet = this.tasksFilter.name !== undefined &&
this.tasksFilter.name !== null &&
this.tasksFilter.name !== ''
const taskStatesFilterSet = this.tasksFilter.states !== undefined &&
this.tasksFilter.states !== null &&
this.tasksFilter.states.length > 0
if (taskNameFilterSet || taskStatesFilterSet) {
this.activeFilters = cloneDeep(this.tasksFilter)
} else {
this.activeFilters = null
}
},
clearInput (event) {
// I don't really like this, but we need to somehow force the 'change detection' to run again once the clear has taken place
this.tasksFilter.name = null
this.$refs.filterNameInput.$el.querySelector('input').dispatchEvent(new Event('keyup'))
return this.tasks.filter(({ task }) => matchNode(task.node, this.tasksFilter.name, this.tasksFilter.states))
}
}
}
Expand Down
Loading

0 comments on commit 09c3827

Please sign in to comment.