Skip to content

Commit 0c9e987

Browse files
authored
Merge pull request #346 from fractal-analytics-platform/jobs-table-fix
Jobs table improvements
2 parents 6367219 + 6e65505 commit 0c9e987

File tree

7 files changed

+149
-113
lines changed

7 files changed

+149
-113
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
This release requires fractal-server 1.4.0.
66

7+
* Added Jobs button in home page (\#346).
8+
* Improved jobs table layout for small screens (\#346).
9+
* Fixed jobs status badge color bug (\#346).
710
* Aligned with fractal-server 1.4.0 API, including trailing slash for endpoints' paths (\#328).
811
* Added spinner during page loading (\#328).
912
* Job pages:

__tests__/JobsList.test.js

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,76 +39,76 @@ describe('JobsList', () => {
3939

4040
const filters = result.getAllByRole('combobox');
4141

42-
const projectFilter = filters[0];
42+
const statusFilter = filters[0];
43+
verifyOptions(statusFilter, ['', 'running', 'done', 'failed', 'submitted']);
44+
const projectFilter = filters[1];
4345
verifyOptions(projectFilter, ['', '1', '2']);
44-
const workflowFilter = filters[1];
46+
const workflowFilter = filters[2];
4547
verifyOptions(workflowFilter, ['', '1', '2']);
46-
const inputDatasetFilter = filters[2];
48+
const inputDatasetFilter = filters[3];
4749
verifyOptions(inputDatasetFilter, ['', '1', '3', '5']);
48-
const outputDatasetFilter = filters[3];
50+
const outputDatasetFilter = filters[4];
4951
verifyOptions(outputDatasetFilter, ['', '2', '4', '6']);
50-
const statusFilter = filters[4];
51-
verifyOptions(statusFilter, ['', 'running', 'done', 'failed', 'submitted']);
5252

5353
// Filter by project
5454
await fireEvent.change(projectFilter, { target: { value: '1' } });
5555
table = result.getByRole('table');
5656
expect(table.querySelectorAll('tbody tr').length).eq(1);
57-
expect(table.querySelectorAll('tbody tr td')[5].textContent).eq('input1');
57+
expect(table.querySelectorAll('tbody tr td')[6].textContent).eq('input1');
5858
await clearFilters(result);
5959

6060
// Filter by workflow
6161
await fireEvent.change(workflowFilter, { target: { value: '2' } });
6262
table = result.getByRole('table');
6363
expect(table.querySelectorAll('tbody tr').length).eq(2);
64-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[5].textContent).eq('input3');
65-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[5].textContent).eq('input2');
64+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[6].textContent).eq('input3');
65+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[6].textContent).eq('input2');
6666
await clearFilters(result);
6767

6868
// Filter by input dataset
6969
await fireEvent.change(inputDatasetFilter, { target: { value: '3' } });
7070
table = result.getByRole('table');
7171
expect(table.querySelectorAll('tbody tr').length).eq(1);
72-
expect(table.querySelectorAll('tbody tr td')[5].textContent).eq('input2');
72+
expect(table.querySelectorAll('tbody tr td')[6].textContent).eq('input2');
7373
await clearFilters(result);
7474

7575
// Filter by output dataset
7676
await fireEvent.change(outputDatasetFilter, { target: { value: '4' } });
7777
table = result.getByRole('table');
7878
expect(table.querySelectorAll('tbody tr').length).eq(1);
79-
expect(table.querySelectorAll('tbody tr td')[6].textContent).eq('output2');
79+
expect(table.querySelectorAll('tbody tr td')[7].textContent).eq('output2');
8080
await clearFilters(result);
8181

8282
// Filter by job status
8383
await fireEvent.change(statusFilter, { target: { value: 'running' } });
8484
table = result.getByRole('table');
8585
expect(table.querySelectorAll('tbody tr').length).eq(1);
86-
expect(table.querySelectorAll('tbody tr td')[5].textContent).eq('input3');
86+
expect(table.querySelectorAll('tbody tr td')[6].textContent).eq('input3');
8787
await clearFilters(result);
8888

8989
// Verify default sorting
9090
table = result.getByRole('table');
91-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[1].textContent).eq(
91+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[2].textContent).eq(
9292
'10/30/2023, 9:30:38 AM'
9393
);
94-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[1].textContent).eq(
94+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[2].textContent).eq(
9595
'10/30/2023, 9:15:38 AM'
9696
);
97-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[1].textContent).eq(
97+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[2].textContent).eq(
9898
'10/30/2023, 9:00:38 AM'
9999
);
100100

101101
// Sort by start date
102-
const startDateSorter = table.querySelector('thead th:nth-child(2)');
102+
const startDateSorter = table.querySelector('thead th:nth-child(3)');
103103
await fireEvent.click(startDateSorter);
104104
table = result.getByRole('table');
105-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[1].textContent).eq(
105+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[2].textContent).eq(
106106
'10/30/2023, 9:00:38 AM'
107107
);
108-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[1].textContent).eq(
108+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[2].textContent).eq(
109109
'10/30/2023, 9:15:38 AM'
110110
);
111-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[1].textContent).eq(
111+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[2].textContent).eq(
112112
'10/30/2023, 9:30:38 AM'
113113
);
114114
});
@@ -127,17 +127,17 @@ describe('JobsList', () => {
127127
});
128128
let table = result.getByRole('table');
129129
expect(table.querySelectorAll('tbody tr').length).eq(3);
130-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[7].textContent).eq('running');
131-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[7].textContent).eq('failed');
132-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[7].textContent).eq('done');
130+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('running');
131+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
132+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
133133

134134
const refreshButton = result.getByRole('button', { name: 'Refresh' });
135135
await fireEvent.click(refreshButton);
136136

137137
table = result.getByRole('table');
138-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[7].textContent).eq('done');
139-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[7].textContent).eq('failed');
140-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[7].textContent).eq('done');
138+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('done');
139+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
140+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
141141
});
142142

143143
it('cancel job', async () => {
@@ -190,19 +190,19 @@ describe('JobsList', () => {
190190
});
191191
let table = result.getByRole('table');
192192
expect(table.querySelectorAll('tbody tr').length).eq(3);
193-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[7].textContent).eq('running');
194-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[7].textContent).eq('failed');
195-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[7].textContent).eq('done');
193+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('running');
194+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
195+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
196196

197197
vi.advanceTimersByTime(3500);
198198
vi.useRealTimers();
199199
// trigger table update
200200
await new Promise(setTimeout);
201201

202202
table = result.getByRole('table');
203-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[7].textContent).eq('done');
204-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[7].textContent).eq('failed');
205-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[7].textContent).eq('done');
203+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('done');
204+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
205+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
206206
} finally {
207207
vi.useRealTimers();
208208
}
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
<script>
2+
/** @type {import('@vincjo/datatables').DataHandler} */
23
export let handler;
4+
/** @type {string} */
35
export let key;
6+
/** @type {string} */
47
export let label;
58
let filterLabel = label;
69
10+
/** @type {import('svelte/store').Writable<{ identifier: string | null, direction: 'asc' | 'desc' | null }>} */
711
let sorted;
812
$: {
913
sorted = handler.getSorted();
1014
if ($sorted.identifier === key) {
1115
if ($sorted.direction === 'asc') {
12-
filterLabel = label + ' ';
16+
filterLabel = label + '&nbsp;';
1317
} else {
14-
filterLabel = label + ' ';
18+
filterLabel = label + '&nbsp;';
1519
}
1620
} else {
17-
filterLabel = label + ' ';
21+
filterLabel = label + '&nbsp;';
1822
}
1923
}
2024
</script>
2125

22-
<th on:click={handler.sort(key)} style="cursor: pointer">{filterLabel}</th>
26+
<th on:click={() => handler.sort(key)} style="cursor: pointer">{@html filterLabel}</th>

src/lib/components/jobs/JobsList.svelte

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,23 @@
162162
<div id="jobUpdatesError" />
163163
<table class="table jobs-table">
164164
<colgroup>
165-
<col id="id-column" />
165+
<col width="110" />
166+
<col width="110" />
167+
<col width="100" />
168+
<col width="100" />
169+
{#if !columnsToHide.includes('project')}
170+
<col width="110" />
171+
{/if}
172+
{#if !columnsToHide.includes('workflow')}
173+
<col width="110" />
174+
{/if}
175+
<col width="110" />
176+
<col width="110" />
166177
</colgroup>
167-
<thead class="table-light text-nowrap">
178+
<thead class="table-light">
168179
<tr>
169-
<Th handler={tableHandler} key="id" label="Id" />
180+
<Th handler={tableHandler} key="status" label="Status" />
181+
<th>Options</th>
170182
<Th handler={tableHandler} key="start_timestamp" label="Start" />
171183
<Th handler={tableHandler} key="end_timestamp" label="End" />
172184
{#if !columnsToHide.includes('project')}
@@ -177,10 +189,17 @@
177189
{/if}
178190
<Th handler={tableHandler} key="input_dataset_id" label="Input dataset" />
179191
<Th handler={tableHandler} key="output_dataset_id" label="Output dataset" />
180-
<Th handler={tableHandler} key="status" label="Status" />
181-
<th>Options</th>
182192
</tr>
183193
<tr>
194+
<th>
195+
<select class="form-control" bind:value={statusFilter}>
196+
<option value="">All</option>
197+
<option value="running">Running</option>
198+
<option value="done">Done</option>
199+
<option value="failed">Failed</option>
200+
<option value="submitted">Submitted</option>
201+
</select>
202+
</th>
184203
<th />
185204
<th />
186205
<th />
@@ -226,24 +245,47 @@
226245
{/each}
227246
</select>
228247
</th>
229-
<th>
230-
<select class="form-control" bind:value={statusFilter}>
231-
<option value="">All</option>
232-
<option value="running">Running</option>
233-
<option value="done">Done</option>
234-
<option value="failed">Failed</option>
235-
<option value="submitted">Submitted</option>
236-
</select>
237-
</th>
238-
<th />
239248
</tr>
240249
</thead>
241250

242251
<tbody>
243252
{#if rows}
244253
{#each $rows as row}
245254
<tr class="align-middle">
246-
<td>{row.id}</td>
255+
<td>
256+
<StatusBadge status={row.status} />
257+
</td>
258+
<td>
259+
<button
260+
class="btn btn-info"
261+
on:click|preventDefault={() =>
262+
jobInfoModal.show(row, getProjectName(row.project_id))}
263+
>
264+
<i class="bi-info-circle" />
265+
Info
266+
</button>
267+
{#if row.status === 'failed' || row.status === 'done'}
268+
<button
269+
class="btn btn-light"
270+
on:click|preventDefault={() => jobLogsModal.show(row.project_id, row.id)}
271+
>
272+
<i class="bi-list-columns-reverse" />
273+
Logs
274+
</button>
275+
<a
276+
class="btn btn-light"
277+
href={`/api/v1/project/${row.project_id}/job/${row.id}/download`}
278+
download={`${row.id}_logs.zip`}
279+
>
280+
<i class="bi-arrow-down-circle" />
281+
</a>
282+
{/if}
283+
{#if row.status === 'running'}
284+
<button class="btn btn-danger" on:click={() => handleJobCancel(row)}>
285+
<i class="bi-x-circle" /> Cancel
286+
</button>
287+
{/if}
288+
</td>
247289
<td>
248290
{row.start_timestamp ? new Date(row.start_timestamp).toLocaleString() : '-'}
249291
</td>
@@ -282,40 +324,6 @@
282324
</a>
283325
{/if}
284326
</td>
285-
<td>
286-
<StatusBadge status={row.status} />
287-
</td>
288-
<td>
289-
<button
290-
class="btn btn-info"
291-
on:click|preventDefault={() =>
292-
jobInfoModal.show(row, getProjectName(row.project_id))}
293-
>
294-
<i class="bi-info-circle" />
295-
Info
296-
</button>
297-
{#if row.status === 'failed' || row.status === 'done'}
298-
<button
299-
class="btn btn-light"
300-
on:click|preventDefault={() => jobLogsModal.show(row.project_id, row.id)}
301-
>
302-
<i class="bi-list-columns-reverse" />
303-
Logs
304-
</button>
305-
<a
306-
class="btn btn-light"
307-
href={`/api/v1/project/${row.project_id}/job/${row.id}/download`}
308-
download={`${row.id}_logs.zip`}
309-
>
310-
<i class="bi-arrow-down-circle" />
311-
</a>
312-
{/if}
313-
{#if row.status === 'running'}
314-
<button class="btn btn-danger" on:click={() => handleJobCancel(row)}>
315-
<i class="bi-x-circle" /> Cancel
316-
</button>
317-
{/if}
318-
</td>
319327
</tr>
320328
{/each}
321329
{/if}
@@ -329,10 +337,13 @@
329337
<style>
330338
.jobs-table {
331339
table-layout: fixed;
332-
word-break: break-all;
333340
}
334341
335-
#id-column {
336-
width: 60px;
342+
.jobs-table thead {
343+
word-break: break-word;
344+
}
345+
346+
.jobs-table tbody {
347+
word-break: break-all;
337348
}
338349
</style>

src/lib/components/jobs/StatusBadge.svelte

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
<script>
22
/** @type {import('$lib/types').JobStatus|null} */
33
export let status = null;
4-
let labelStyle = null;
4+
$: labelStyle = getLabelStyle(status);
55
6-
switch (status) {
7-
case 'submitted':
8-
labelStyle = 'badge text-bg-info';
9-
break;
10-
case 'running':
11-
labelStyle = 'badge text-bg-primary';
12-
break;
13-
case 'done':
14-
labelStyle = 'badge text-bg-success';
15-
break;
16-
case 'failed':
17-
labelStyle = 'badge text-bg-danger';
18-
break;
6+
/**
7+
* @param {import('$lib/types').JobStatus|null} status
8+
*/
9+
function getLabelStyle(status) {
10+
switch (status) {
11+
case 'submitted':
12+
return 'badge text-bg-info';
13+
case 'running':
14+
return 'badge text-bg-primary';
15+
case 'done':
16+
return 'badge text-bg-success';
17+
case 'failed':
18+
return 'badge text-bg-danger';
19+
}
1920
}
2021
</script>
2122

0 commit comments

Comments
 (0)