|
4 | 4 | import { displayStandardErrorAlert, getAlertErrorFromResponse } from '$lib/common/errors'; |
5 | 5 | import { sortUsers } from '$lib/components/admin/user_utilities'; |
6 | 6 | import Modal from '$lib/components/common/Modal.svelte'; |
| 7 | + import Paginator from '$lib/components/common/Paginator.svelte'; |
7 | 8 | import JobsList from '$lib/components/v2/jobs/JobsList.svelte'; |
8 | 9 | import { normalizePayload } from 'fractal-components'; |
9 | 10 |
|
|
12 | 13 |
|
13 | 14 | let searched = $state(false); |
14 | 15 | let searching = $state(false); |
| 16 | + let processingCsv = $state(false); |
15 | 17 | /** @type {import('$lib/components/common/StandardErrorAlert.svelte').default|undefined} */ |
16 | 18 | let searchErrorAlert; |
17 | 19 |
|
18 | 20 | /** @type {JobsList|undefined} */ |
19 | 21 | 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); |
22 | 27 |
|
23 | 28 | let status = $state(); |
24 | 29 | let userId = $state(); |
|
42 | 47 | * @returns {Promise<import('fractal-components/types/api').ApplyWorkflowV2[]>} |
43 | 48 | */ |
44 | 49 | async function jobUpdater() { |
| 50 | + if (!jobs) { |
| 51 | + return []; |
| 52 | + } |
| 53 | +
|
45 | 54 | /** @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'); |
47 | 56 | /** @type {import('fractal-components/types/api').ApplyWorkflowV2[]} */ |
48 | 57 | const updatedJobs = []; |
49 | 58 | for (const job of jobsToCheck) { |
|
52 | 61 | url.searchParams.append('id', job.id.toString()); |
53 | 62 | const response = await fetch(url); |
54 | 63 | if (response.ok) { |
55 | | - updatedJobs.push((await response.json())[0]); |
| 64 | + const { items } = await response.json(); |
| 65 | + updatedJobs.push(items[0]); |
56 | 66 | } |
57 | 67 | } |
58 | | - jobs = jobs.map((j) => { |
| 68 | + jobs.items = jobs.items.map((j) => { |
59 | 69 | if (j.status === 'failed') { |
60 | 70 | // The admin has manually updated the job status while the AJAX call was in progress |
61 | 71 | return j; |
62 | 72 | } |
63 | 73 | const updatedJob = updatedJobs.find((uj) => uj.id === j.id); |
64 | 74 | return updatedJob ?? j; |
65 | 75 | }); |
66 | | - return jobs; |
| 76 | + return jobs.items; |
67 | 77 | } |
68 | 78 |
|
69 | | - async function searchJobs() { |
| 79 | + /** |
| 80 | + * @param {number} selectedPage |
| 81 | + * @param {number} selectedPageSize |
| 82 | + */ |
| 83 | + async function searchJobs(selectedPage, selectedPageSize) { |
70 | 84 | searching = true; |
71 | 85 | try { |
72 | 86 | if (searchErrorAlert) { |
73 | 87 | searchErrorAlert.hide(); |
74 | 88 | } |
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()); |
111 | 92 | const response = await fetch(url); |
112 | 93 | if (!response.ok) { |
113 | 94 | searchErrorAlert = displayStandardErrorAlert( |
|
118 | 99 | } |
119 | 100 | searched = true; |
120 | 101 | 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 | + } |
122 | 108 | } finally { |
123 | 109 | searching = false; |
124 | 110 | } |
125 | 111 | } |
126 | 112 |
|
| 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 | +
|
127 | 153 | function resetSearchFields() { |
128 | 154 | if (searchErrorAlert) { |
129 | 155 | searchErrorAlert.hide(); |
|
143 | 169 | workflowId = ''; |
144 | 170 | datasetId = ''; |
145 | 171 | searched = false; |
146 | | - jobs = []; |
| 172 | + jobs = undefined; |
147 | 173 | jobsListComponent?.setJobs([]); |
148 | 174 | } |
149 | 175 |
|
150 | 176 | async function downloadCSV() { |
| 177 | + if (!jobs) { |
| 178 | + return; |
| 179 | + } |
| 180 | +
|
| 181 | + processingCsv = true; |
| 182 | +
|
151 | 183 | const header = [ |
152 | 184 | 'id', |
153 | 185 | 'status', |
|
164 | 196 | 'first_task_index', |
165 | 197 | 'last_task_index' |
166 | 198 | ]; |
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) => [ |
168 | 217 | job.id, |
169 | 218 | job.status, |
170 | 219 | job.start_timestamp, |
|
180 | 229 | job.first_task_index, |
181 | 230 | job.last_task_index |
182 | 231 | ]); |
| 232 | +
|
183 | 233 | const csv = arrayToCsv([header, ...rows]); |
184 | 234 | downloadBlob(csv, 'jobs.csv', 'text/csv;charset=utf-8;'); |
| 235 | +
|
| 236 | + processingCsv = false; |
185 | 237 | } |
186 | 238 |
|
187 | 239 | /** @type {Modal|undefined} */ |
|
202 | 254 | async function updateJobStatus() { |
203 | 255 | statusModal?.confirmAndHide( |
204 | 256 | async () => { |
| 257 | + if (!jobs) { |
| 258 | + return; |
| 259 | + } |
| 260 | +
|
205 | 261 | updatingStatus = true; |
206 | 262 | const jobId = /** @type {import('fractal-components/types/api').ApplyWorkflowV2} */ ( |
207 | 263 | jobInEditing |
|
221 | 277 | throw await getAlertErrorFromResponse(response); |
222 | 278 | } |
223 | 279 |
|
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); |
226 | 282 | }, |
227 | 283 | () => { |
228 | 284 | updatingStatus = false; |
|
357 | 413 | </div> |
358 | 414 | </div> |
359 | 415 |
|
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}> |
361 | 417 | {#if searching} |
362 | 418 | <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> |
363 | 419 | {:else} |
|
372 | 428 | <div id="searchError" class="mt-3"></div> |
373 | 429 |
|
374 | 430 | <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} |
378 | 434 | <JobsList {jobUpdater} bind:this={jobsListComponent} admin={true}> |
379 | 435 | {#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 |
382 | 443 | </button> |
383 | 444 | {/snippet} |
384 | 445 | {#snippet editStatus(row)} |
|
394 | 455 | {/if} |
395 | 456 | {/snippet} |
396 | 457 | </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} |
397 | 468 | </div> |
398 | 469 | </div> |
399 | 470 |
|
|
0 commit comments