Skip to content

Commit 9fa8abe

Browse files
committed
Added paginator to admin tasks and jobs pages
1 parent d2e965c commit 9fa8abe

File tree

6 files changed

+217
-138
lines changed

6 files changed

+217
-138
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* Added support for oneOf and discriminator (\#867);
1818
* Added resource filter on admin task and task-group pages (\#867);
1919
* Handled `FRACTAL_DEFAULT_GROUP_NAME` environment variable, to specify if a default group is used (\#870);
20+
* Renamed "Restart workflow" to "Reset workflow" (\#876);
21+
* Added paginator to admin tasks and jobs pages (\#876);
2022

2123
# 1.20.0
2224

playwright.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export default defineConfig({
8888

8989
webServer: [
9090
{
91-
command: './tests/start-test-server.sh --branch main',
91+
command: './tests/start-test-server.sh --branch pagination',
9292
port: 8000,
9393
waitForPort: true,
9494
stdout: 'pipe',

src/routes/v2/admin/jobs/+page.svelte

Lines changed: 125 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { displayStandardErrorAlert, getAlertErrorFromResponse } from '$lib/common/errors';
55
import { sortUsers } from '$lib/components/admin/user_utilities';
66
import Modal from '$lib/components/common/Modal.svelte';
7+
import Paginator from '$lib/components/common/Paginator.svelte';
78
import JobsList from '$lib/components/v2/jobs/JobsList.svelte';
89
import { normalizePayload } from 'fractal-components';
910
@@ -12,13 +13,17 @@
1213
1314
let searched = $state(false);
1415
let searching = $state(false);
16+
let processingCsv = $state(false);
1517
/** @type {import('$lib/components/common/StandardErrorAlert.svelte').default|undefined} */
1618
let searchErrorAlert;
1719
1820
/** @type {JobsList|undefined} */
1921
let jobsListComponent = $state();
20-
/** @type {import('fractal-components/types/api').ApplyWorkflowV2[]} */
21-
let jobs = $state([]);
22+
/** @type {import('fractal-components/types/api').Pagination<import('fractal-components/types/api').ApplyWorkflowV2> | undefined} */
23+
let jobs = $state();
24+
let currentPage = $state(1);
25+
let pageSize = $state(10);
26+
let totalCount = $state(0);
2227
2328
let status = $state();
2429
let userId = $state();
@@ -42,8 +47,12 @@
4247
* @returns {Promise<import('fractal-components/types/api').ApplyWorkflowV2[]>}
4348
*/
4449
async function jobUpdater() {
50+
if (!jobs) {
51+
return [];
52+
}
53+
4554
/** @type {import('fractal-components/types/api').ApplyWorkflowV2[]} */
46-
const jobsToCheck = jobs.filter((j) => j.status === 'submitted');
55+
const jobsToCheck = jobs.items.filter((j) => j.status === 'submitted');
4756
/** @type {import('fractal-components/types/api').ApplyWorkflowV2[]} */
4857
const updatedJobs = [];
4958
for (const job of jobsToCheck) {
@@ -52,62 +61,34 @@
5261
url.searchParams.append('id', job.id.toString());
5362
const response = await fetch(url);
5463
if (response.ok) {
55-
updatedJobs.push((await response.json())[0]);
64+
const { items } = await response.json();
65+
updatedJobs.push(items[0]);
5666
}
5767
}
58-
jobs = jobs.map((j) => {
68+
jobs.items = jobs.items.map((j) => {
5969
if (j.status === 'failed') {
6070
// The admin has manually updated the job status while the AJAX call was in progress
6171
return j;
6272
}
6373
const updatedJob = updatedJobs.find((uj) => uj.id === j.id);
6474
return updatedJob ?? j;
6575
});
66-
return jobs;
76+
return jobs.items;
6777
}
6878
69-
async function searchJobs() {
79+
/**
80+
* @param {number} selectedPage
81+
* @param {number} selectedPageSize
82+
*/
83+
async function searchJobs(selectedPage, selectedPageSize) {
7084
searching = true;
7185
try {
7286
if (searchErrorAlert) {
7387
searchErrorAlert.hide();
7488
}
75-
const url = new URL('/api/admin/v2/job', window.location.origin);
76-
url.searchParams.append('log', 'false');
77-
if (status) {
78-
url.searchParams.append('status', status);
79-
}
80-
if (userId) {
81-
url.searchParams.append('user_id', userId);
82-
}
83-
if (jobId) {
84-
url.searchParams.append('id', jobId);
85-
}
86-
const startTimestampMin = getTimestamp(startDateMin, startTimeMin);
87-
if (startTimestampMin) {
88-
url.searchParams.append('start_timestamp_min', startTimestampMin);
89-
}
90-
const startTimestampMax = getTimestamp(startDateMax, startTimeMax);
91-
if (startTimestampMax) {
92-
url.searchParams.append('start_timestamp_max', startTimestampMax);
93-
}
94-
const endTimestampMin = getTimestamp(endDateMin, endTimeMin);
95-
if (endTimestampMin) {
96-
url.searchParams.append('end_timestamp_min', endTimestampMin);
97-
}
98-
const endTimestampMax = getTimestamp(endDateMax, endTimeMax);
99-
if (endTimestampMax) {
100-
url.searchParams.append('end_timestamp_max', endTimestampMax);
101-
}
102-
if (projectId) {
103-
url.searchParams.append('project_id', projectId);
104-
}
105-
if (workflowId) {
106-
url.searchParams.append('workflow_id', workflowId);
107-
}
108-
if (datasetId) {
109-
url.searchParams.append('dataset_id', datasetId);
110-
}
89+
const url = getBaseJobsSearchUrl();
90+
url.searchParams.append('page', selectedPage.toString());
91+
url.searchParams.append('page_size', selectedPageSize.toString());
11192
const response = await fetch(url);
11293
if (!response.ok) {
11394
searchErrorAlert = displayStandardErrorAlert(
@@ -118,12 +99,57 @@
11899
}
119100
searched = true;
120101
jobs = await response.json();
121-
jobsListComponent?.setJobs(jobs);
102+
if (jobs) {
103+
currentPage = jobs.current_page;
104+
pageSize = jobs.page_size;
105+
totalCount = jobs.total_count;
106+
jobsListComponent?.setJobs(jobs.items);
107+
}
122108
} finally {
123109
searching = false;
124110
}
125111
}
126112
113+
function getBaseJobsSearchUrl() {
114+
const url = new URL('/api/admin/v2/job', window.location.origin);
115+
url.searchParams.append('log', 'false');
116+
if (status) {
117+
url.searchParams.append('status', status);
118+
}
119+
if (userId) {
120+
url.searchParams.append('user_id', userId);
121+
}
122+
if (jobId) {
123+
url.searchParams.append('id', jobId);
124+
}
125+
const startTimestampMin = getTimestamp(startDateMin, startTimeMin);
126+
if (startTimestampMin) {
127+
url.searchParams.append('start_timestamp_min', startTimestampMin);
128+
}
129+
const startTimestampMax = getTimestamp(startDateMax, startTimeMax);
130+
if (startTimestampMax) {
131+
url.searchParams.append('start_timestamp_max', startTimestampMax);
132+
}
133+
const endTimestampMin = getTimestamp(endDateMin, endTimeMin);
134+
if (endTimestampMin) {
135+
url.searchParams.append('end_timestamp_min', endTimestampMin);
136+
}
137+
const endTimestampMax = getTimestamp(endDateMax, endTimeMax);
138+
if (endTimestampMax) {
139+
url.searchParams.append('end_timestamp_max', endTimestampMax);
140+
}
141+
if (projectId) {
142+
url.searchParams.append('project_id', projectId);
143+
}
144+
if (workflowId) {
145+
url.searchParams.append('workflow_id', workflowId);
146+
}
147+
if (datasetId) {
148+
url.searchParams.append('dataset_id', datasetId);
149+
}
150+
return url;
151+
}
152+
127153
function resetSearchFields() {
128154
if (searchErrorAlert) {
129155
searchErrorAlert.hide();
@@ -143,11 +169,17 @@
143169
workflowId = '';
144170
datasetId = '';
145171
searched = false;
146-
jobs = [];
172+
jobs = undefined;
147173
jobsListComponent?.setJobs([]);
148174
}
149175
150176
async function downloadCSV() {
177+
if (!jobs) {
178+
return;
179+
}
180+
181+
processingCsv = true;
182+
151183
const header = [
152184
'id',
153185
'status',
@@ -164,7 +196,24 @@
164196
'first_task_index',
165197
'last_task_index'
166198
];
167-
const rows = jobs.map((job) => [
199+
200+
const url = getBaseJobsSearchUrl();
201+
const response = await fetch(url);
202+
if (!response.ok) {
203+
searchErrorAlert = displayStandardErrorAlert(
204+
await getAlertErrorFromResponse(response),
205+
'searchError'
206+
);
207+
processingCsv = false;
208+
return;
209+
}
210+
211+
const { items } =
212+
/** @type {import('fractal-components/types/api').Pagination<import('fractal-components/types/api').ApplyWorkflowV2>} */ (
213+
await response.json()
214+
);
215+
216+
const rows = items.map((job) => [
168217
job.id,
169218
job.status,
170219
job.start_timestamp,
@@ -180,8 +229,11 @@
180229
job.first_task_index,
181230
job.last_task_index
182231
]);
232+
183233
const csv = arrayToCsv([header, ...rows]);
184234
downloadBlob(csv, 'jobs.csv', 'text/csv;charset=utf-8;');
235+
236+
processingCsv = false;
185237
}
186238
187239
/** @type {Modal|undefined} */
@@ -202,6 +254,10 @@
202254
async function updateJobStatus() {
203255
statusModal?.confirmAndHide(
204256
async () => {
257+
if (!jobs) {
258+
return;
259+
}
260+
205261
updatingStatus = true;
206262
const jobId = /** @type {import('fractal-components/types/api').ApplyWorkflowV2} */ (
207263
jobInEditing
@@ -221,8 +277,8 @@
221277
throw await getAlertErrorFromResponse(response);
222278
}
223279
224-
jobs = jobs.map((j) => (j.id === jobId ? { ...j, status: 'failed' } : j));
225-
jobsListComponent?.setJobs(jobs);
280+
jobs.items = jobs.items.map((j) => (j.id === jobId ? { ...j, status: 'failed' } : j));
281+
jobsListComponent?.setJobs(jobs.items);
226282
},
227283
() => {
228284
updatingStatus = false;
@@ -357,7 +413,7 @@
357413
</div>
358414
</div>
359415
360-
<button class="btn btn-primary mt-4" onclick={searchJobs} disabled={searching}>
416+
<button class="btn btn-primary mt-4" onclick={() => searchJobs(1, pageSize)} disabled={searching}>
361417
{#if searching}
362418
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
363419
{:else}
@@ -372,13 +428,18 @@
372428
<div id="searchError" class="mt-3"></div>
373429
374430
<div class:d-none={!searched}>
375-
<p class="text-center">
376-
The query returned {jobs.length} matching {jobs.length !== 1 ? 'jobs' : 'job'}
377-
</p>
431+
{#if jobs && jobs.total_count === 0}
432+
<p class="text-center">The query returned 0 matching jobs</p>
433+
{/if}
378434
<JobsList {jobUpdater} bind:this={jobsListComponent} admin={true}>
379435
{#snippet buttons()}
380-
<button class="btn btn-outline-secondary" onclick={downloadCSV}>
381-
<i class="bi-download"></i> Download CSV
436+
<button class="btn btn-outline-secondary" onclick={downloadCSV} disabled={processingCsv}>
437+
{#if processingCsv}
438+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
439+
{:else}
440+
<i class="bi-download"></i>
441+
{/if}
442+
Download CSV
382443
</button>
383444
{/snippet}
384445
{#snippet editStatus(row)}
@@ -394,6 +455,16 @@
394455
{/if}
395456
{/snippet}
396457
</JobsList>
458+
459+
{#if jobs && jobs.total_count > 0}
460+
<Paginator
461+
{currentPage}
462+
{pageSize}
463+
{totalCount}
464+
singleLine={true}
465+
onPageChange={(currentPage, pageSize) => searchJobs(currentPage, pageSize)}
466+
/>
467+
{/if}
397468
</div>
398469
</div>
399470

0 commit comments

Comments
 (0)