Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Centralise task filtering into own component; list task states in logical order #1123

Merged
merged 4 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps move into src/utils?

* 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