Skip to content

Commit dac2399

Browse files
authored
Table view: sort empty entries last regardless of sort order (cylc#1762)
1 parent 5fd5523 commit dac2399

File tree

6 files changed

+186
-116
lines changed

6 files changed

+186
-116
lines changed

src/components/cylc/common/sort.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
33
*
44
* This program is free software: you can redistribute it and/or modify
@@ -36,15 +36,14 @@
3636
* @constructor
3737
*/
3838
export const DEFAULT_COMPARATOR = (left, right) => {
39-
return left.toLowerCase()
40-
.localeCompare(
41-
right.toLowerCase(),
42-
undefined,
43-
{
44-
numeric: true,
45-
sensitivity: 'base'
46-
}
47-
)
39+
return left.toLowerCase().localeCompare(
40+
right.toLowerCase(),
41+
undefined,
42+
{
43+
numeric: true,
44+
sensitivity: 'base',
45+
}
46+
)
4847
}
4948

5049
/**

src/components/cylc/table/Table.vue

Lines changed: 100 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

1818
<template>
1919
<v-data-table
20-
:headers="$options.headers"
20+
:headers="headers"
2121
:items="tasks"
2222
item-value="task.id"
2323
multi-sort
@@ -62,7 +62,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
6262
}"
6363
>
6464
<v-icon
65-
:icon="$options.icons.mdiChevronDown"
65+
:icon="icons.mdiChevronDown"
6666
size="large"
6767
/>
6868
</v-btn>
@@ -95,17 +95,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
9595
</tr>
9696
</template>
9797
<template v-slot:bottom>
98-
<v-data-table-footer :itemsPerPageOptions="$options.itemsPerPageOptions" />
98+
<v-data-table-footer :itemsPerPageOptions="itemsPerPageOptions" />
9999
</template>
100100
</v-data-table>
101101
</template>
102102

103103
<script>
104+
import { ref, watch } from 'vue'
104105
import Task from '@/components/cylc/Task.vue'
105106
import Job from '@/components/cylc/Job.vue'
106107
import { mdiChevronDown } from '@mdi/js'
107108
import { DEFAULT_COMPARATOR } from '@/components/cylc/common/sort'
108-
import { datetimeComparator } from '@/components/cylc/table/sort'
109+
import {
110+
datetimeComparator,
111+
numberComparator,
112+
} from '@/components/cylc/table/sort'
109113
import { dtMean } from '@/utils/tasks'
110114
import { useCyclePointsOrderDesc } from '@/composables/localStorage'
111115
import {
@@ -150,87 +154,104 @@ export default {
150154
151155
const itemsPerPage = useInitialOptions('itemsPerPage', { props, emit }, 50)
152156
157+
const headers = ref([
158+
{
159+
title: 'Task',
160+
key: 'task.name',
161+
sortable: true,
162+
sortFunc: DEFAULT_COMPARATOR
163+
},
164+
{
165+
title: 'Jobs',
166+
key: 'data-table-expand',
167+
sortable: false
168+
},
169+
{
170+
title: 'Cycle Point',
171+
key: 'task.tokens.cycle',
172+
sortable: true,
173+
sortFunc: DEFAULT_COMPARATOR,
174+
},
175+
{
176+
title: 'Platform',
177+
key: 'latestJob.node.platform',
178+
sortable: true,
179+
sortFunc: DEFAULT_COMPARATOR,
180+
},
181+
{
182+
title: 'Job Runner',
183+
key: 'latestJob.node.jobRunnerName',
184+
sortable: true,
185+
sortFunc: DEFAULT_COMPARATOR,
186+
},
187+
{
188+
title: 'Job ID',
189+
key: 'latestJob.node.jobId',
190+
sortable: true,
191+
sortFunc: DEFAULT_COMPARATOR,
192+
},
193+
{
194+
title: 'Submit',
195+
key: 'latestJob.node.submittedTime',
196+
sortable: true,
197+
sortFunc: datetimeComparator,
198+
},
199+
{
200+
title: 'Start',
201+
key: 'latestJob.node.startedTime',
202+
sortable: true,
203+
sortFunc: datetimeComparator,
204+
},
205+
{
206+
title: 'Finish',
207+
key: 'latestJob.node.finishedTime',
208+
sortable: true,
209+
sortFunc: datetimeComparator,
210+
},
211+
{
212+
title: 'Run Time',
213+
key: 'task.node.task.meanElapsedTime',
214+
sortable: true,
215+
sortFunc: numberComparator,
216+
},
217+
])
218+
219+
// Ensure that whenever a sort order changes, empty/nullish values are
220+
// always sorted last regardless.
221+
watch(
222+
sortBy,
223+
(val) => {
224+
for (const { key, order } of val) {
225+
const header = headers.value.find((x) => x.key === key)
226+
header.sort = (a, b) => {
227+
if (!a && !b) return 0
228+
if (!a) return order === 'asc' ? 1 : -1
229+
if (!b) return order === 'asc' ? -1 : 1
230+
return header.sortFunc(a, b)
231+
}
232+
}
233+
},
234+
{ deep: true, immediate: true }
235+
)
236+
153237
return {
154238
dtMean,
155239
itemsPerPage,
156240
page,
157241
sortBy,
242+
headers,
243+
icons: {
244+
mdiChevronDown
245+
},
246+
itemsPerPageOptions: [
247+
{ value: 10, title: '10' },
248+
{ value: 20, title: '20' },
249+
{ value: 50, title: '50' },
250+
{ value: 100, title: '100' },
251+
{ value: 200, title: '200' },
252+
{ value: -1, title: 'All' },
253+
],
158254
}
159255
},
160-
161-
headers: [
162-
{
163-
title: 'Task',
164-
key: 'task.name',
165-
sortable: true,
166-
sort: DEFAULT_COMPARATOR
167-
},
168-
{
169-
title: 'Jobs',
170-
key: 'data-table-expand',
171-
sortable: false
172-
},
173-
{
174-
title: 'Cycle Point',
175-
key: 'task.tokens.cycle',
176-
sortable: true,
177-
sort: (a, b) => DEFAULT_COMPARATOR(String(a ?? ''), String(b ?? ''))
178-
},
179-
{
180-
title: 'Platform',
181-
key: 'latestJob.node.platform',
182-
sortable: true,
183-
sort: (a, b) => DEFAULT_COMPARATOR(a ?? '', b ?? '')
184-
},
185-
{
186-
title: 'Job Runner',
187-
key: 'latestJob.node.jobRunnerName',
188-
sortable: true,
189-
sort: (a, b) => DEFAULT_COMPARATOR(a ?? '', b ?? '')
190-
},
191-
{
192-
title: 'Job ID',
193-
key: 'latestJob.node.jobId',
194-
sortable: true,
195-
sort: (a, b) => DEFAULT_COMPARATOR(a ?? '', b ?? '')
196-
},
197-
{
198-
title: 'Submit',
199-
key: 'latestJob.node.submittedTime',
200-
sortable: true,
201-
sort: (a, b) => datetimeComparator(a ?? '', b ?? '')
202-
},
203-
{
204-
title: 'Start',
205-
key: 'latestJob.node.startedTime',
206-
sortable: true,
207-
sort: (a, b) => datetimeComparator(a ?? '', b ?? '')
208-
},
209-
{
210-
title: 'Finish',
211-
key: 'latestJob.node.finishedTime',
212-
sortable: true,
213-
sort: (a, b) => datetimeComparator(a ?? '', b ?? '')
214-
},
215-
{
216-
title: 'Run Time',
217-
key: 'task.node.task.meanElapsedTime',
218-
sortable: true,
219-
sort: (a, b) => parseInt(a ?? 0) - parseInt(b ?? 0)
220-
},
221-
],
222-
223-
icons: {
224-
mdiChevronDown
225-
},
226-
227-
itemsPerPageOptions: [
228-
{ value: 10, title: '10' },
229-
{ value: 20, title: '20' },
230-
{ value: 50, title: '50' },
231-
{ value: 100, title: '100' },
232-
{ value: 200, title: '200' },
233-
{ value: -1, title: 'All' }
234-
]
235256
}
236257
</script>

src/components/cylc/table/sort.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
33
*
44
* This program is free software: you can redistribute it and/or modify
@@ -16,17 +16,23 @@
1616
*/
1717

1818
/**
19-
* Comparator function for sorting datetime strings. Note: nullish or empty
20-
* strings are treated as infinity.
19+
* Comparator function for sorting datetime strings.
2120
*
22-
* @export
23-
* @param {*} a - The first element for comparison.
24-
* @param {*} b - The second element for comparison.
21+
* @param {string} a - The first element for comparison.
22+
* @param {string} b - The second element for comparison.
2523
* @return {number} A number > 0 if a > b, or < 0 if a < b, or 0 if a === b
2624
*/
2725
export function datetimeComparator (a, b) {
28-
a = (a ?? '') === '' ? Infinity : new Date(a).getTime()
29-
b = (b ?? '') === '' ? Infinity : new Date(b).getTime()
30-
// Avoid return NaN for a === b === Infinity
31-
return a === b ? 0 : a - b
26+
return new Date(a) - new Date(b)
27+
}
28+
29+
/**
30+
* Comparator function for sorting numbers.
31+
*
32+
* @param {number} a
33+
* @param {number} b
34+
* @returns {number}
35+
*/
36+
export function numberComparator (a, b) {
37+
return a - b
3238
}

tests/e2e/specs/table.cy.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,26 +100,29 @@ describe('Table view', () => {
100100
.should('be.visible')
101101
})
102102
it('displays and sorts mean run time', () => {
103-
cy
104-
// sort dt-mean ascending
105-
.get('.c-table')
103+
// sort dt-mean ascending
104+
cy.get('.c-table')
106105
.contains('th', 'Run Time').as('dTHeader')
107106
.click()
108-
109-
// check 0 is at the top (1st row, 10th column)
107+
// (1st row, 10th column)
110108
.get('tbody > :nth-child(1) > :nth-child(10)')
109+
.contains('00:00:04')
110+
.get('tbody > :nth-child(2) > :nth-child(10)')
111+
.contains('00:00:12')
112+
.get('tbody > :nth-child(3) > :nth-child(10)')
111113
.should(($ele) => {
112-
expect($ele.text().trim()).equal('') // no value sorted first
114+
expect($ele.text().trim()).equal('') // no value sorted after numbers
113115
})
114-
115116
// sort dt-mean descending
116-
.get('@dTHeader')
117+
cy.get('@dTHeader')
117118
.click()
118-
119-
// check 7 is at the top (1st row, 10th column)
120119
.get('tbody > :nth-child(1) > :nth-child(10)')
120+
.contains('00:00:12')
121+
.get('tbody > :nth-child(2) > :nth-child(10)')
122+
.contains('00:00:04')
123+
.get('tbody > :nth-child(3) > :nth-child(10)')
121124
.should(($ele) => {
122-
expect($ele.text().trim()).equal('00:00:12')
125+
expect($ele.text().trim()).equal('') // no value still sorted after numbers
123126
})
124127
})
125128
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
import { DEFAULT_COMPARATOR } from '@/components/cylc/common/sort'
19+
20+
describe('DEFAULT_COMPARATOR', () => {
21+
it.each([
22+
['a', 'b', -1],
23+
['b', 'a', 1],
24+
['a', 'a', 0],
25+
['A', 'a', 0],
26+
['A', 'b', -1],
27+
['1', '2', -1],
28+
['01', '1', 0],
29+
['20000101T0000Z', '20000101T0001Z', -1],
30+
])('(%o, %o) -> %o', (a, b, expected) => {
31+
expect(DEFAULT_COMPARATOR(a, b)).toEqual(expected)
32+
})
33+
})

0 commit comments

Comments
 (0)