Skip to content

Commit 5a575f2

Browse files
committed
Added job status monitoring in workflow page
1 parent 1a3c4d5 commit 5a575f2

File tree

2 files changed

+183
-57
lines changed

2 files changed

+183
-57
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script>
2+
/** @type {import('$lib/types').JobStatus|undefined} */
3+
export let status;
4+
</script>
5+
6+
{#if status}
7+
<span title={status}>
8+
{#if status === 'submitted'}
9+
<i class="bi bi-hourglass text-secondary job-status-submitted" />
10+
{:else if status === 'running'}
11+
<div class="spinner-border spinner-border-sm text-primary job-status-icon-running" role="status">
12+
<span class="visually-hidden">Loading...</span>
13+
</div>
14+
{:else if status === 'done'}
15+
<i class="job-status-icon bi bi-check text-success" />
16+
{:else if status === 'failed'}
17+
<i class="job-status-icon bi bi-x text-danger" />
18+
{/if}
19+
</span>
20+
{/if}
21+
22+
<style>
23+
:global(.job-status-icon) {
24+
font-size: 160%;
25+
font-weight: bold;
26+
margin: 0 -5px -5px -5px;
27+
line-height: 0;
28+
display: block;
29+
}
30+
31+
:global(.active .job-status-icon) {
32+
background-color: #fff;
33+
border-radius: 50%;
34+
}
35+
36+
:global(.active .job-status-icon-running, .active .job-status-submitted) {
37+
color: #fff !important;
38+
}
39+
</style>

src/routes/projects/[projectId]/workflows/experimental/[workflowId]/+page.svelte

Lines changed: 144 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script>
2+
import { env } from '$env/dynamic/public';
23
import { onMount } from 'svelte';
34
import { writable } from 'svelte/store';
4-
import { goto, beforeNavigate } from '$app/navigation';
5+
import { beforeNavigate } from '$app/navigation';
56
import { page } from '$app/stores';
67
import ArgumentForm from '$lib/components/workflow/ArgumentForm.svelte';
78
import ConfirmActionButton from '$lib/components/common/ConfirmActionButton.svelte';
@@ -14,17 +15,24 @@
1415
import StandardDismissableAlert from '$lib/components/common/StandardDismissableAlert.svelte';
1516
import VersionUpdate from '$lib/components/workflow/VersionUpdate.svelte';
1617
import { getAllNewVersions } from '$lib/components/workflow/version-checker';
18+
import JobStatusIcon from '$lib/components/jobs/JobStatusIcon.svelte';
19+
20+
/** @type {import('$lib/types').Workflow} */
21+
let workflow = $page.data.workflow;
22+
/** @type {import('$lib/types').Project} */
23+
let project = $page.data.project;
24+
/** @type {import('$lib/types').Dataset[]} */
25+
let datasets = $page.data.datasets;
26+
// List of available tasks to be inserted into workflow
27+
let availableTasks = [];
28+
29+
/** @type {number|undefined} */
30+
let selectedInputDatasetId;
31+
/** @type {number|undefined} */
32+
let selectedOutputDatasetId;
1733
18-
// Workflow
19-
/** @type {import('$lib/types').Workflow|undefined} */
20-
let workflow = undefined;
2134
/** @type {import('$lib/components/common/StandardErrorAlert.svelte').default|undefined} */
2235
let workflowErrorAlert = undefined;
23-
// Project context properties
24-
let project = undefined;
25-
let datasets = [];
26-
// List of available tasks to be inserted into workflow
27-
let availableTasks = [];
2836
2937
/** @type {import('svelte/types/runtime/store').Writable<import('$lib/types').WorkflowTask|undefined>} */
3038
let workflowTaskContext = writable(undefined);
@@ -34,8 +42,6 @@
3442
let selectedWorkflowTask = undefined;
3543
let originalMetaProperties = {};
3644
let checkingConfiguration = false;
37-
let inputDatasetControl = '';
38-
let outputDatasetControl = '';
3945
let workerInitControl = '';
4046
let firstTaskIndexControl = '';
4147
let lastTaskIndexControl = '';
@@ -69,7 +75,7 @@
6975
/** @type {{ [id: string]: import('$lib/types').Task[] }} */
7076
let newVersionsMap = {};
7177
72-
$: updatableWorkflowList = workflow?.task_list || [];
78+
$: updatableWorkflowList = workflow.task_list || [];
7379
7480
workflowTaskContext.subscribe((value) => {
7581
selectedWorkflowTask = value;
@@ -81,11 +87,13 @@
8187
}
8288
});
8389
90+
const updateJobsInterval = env.PUBLIC_UPDATE_JOBS_INTERVAL
91+
? parseInt(env.PUBLIC_UPDATE_JOBS_INTERVAL)
92+
: 3000;
93+
8494
onMount(async () => {
85-
workflow = $page.data.workflow;
86-
project = $page.data.project;
87-
datasets = $page.data.datasets;
88-
checkNewVersions();
95+
await loadJobsStatus();
96+
await checkNewVersions();
8997
});
9098
9199
beforeNavigate((navigation) => {
@@ -401,12 +409,12 @@
401409
if (!workflow) {
402410
return;
403411
}
404-
if (inputDatasetControl === '') {
412+
if (selectedInputDatasetId === undefined) {
405413
// Preliminary check: if inputDatasetControl is not set, raise an error
406414
let message = 'Input dataset is required. Select one from the list.';
407415
console.error(message);
408416
runWorkflowModal.displayErrorAlert(message);
409-
} else if (outputDatasetControl === '') {
417+
} else if (selectedOutputDatasetId === undefined) {
410418
// Preliminary check: if outputDatasetControl is not set, raise an error
411419
let message = 'Output dataset is required. Select one from the list.';
412420
console.error(message);
@@ -423,7 +431,7 @@
423431
headers.set('Content-Type', 'application/json');
424432
425433
const response = await fetch(
426-
`/api/v1/project/${project.id}/workflow/${workflow.id}/apply?input_dataset_id=${inputDatasetControl}&output_dataset_id=${outputDatasetControl}`,
434+
`/api/v1/project/${project.id}/workflow/${workflow.id}/apply?input_dataset_id=${selectedInputDatasetId}&output_dataset_id=${selectedOutputDatasetId}`,
427435
{
428436
method: 'POST',
429437
credentials: 'include',
@@ -438,11 +446,7 @@
438446
// @ts-ignore
439447
// eslint-disable-next-line
440448
runWorkflowModal.toggle();
441-
// Navigate to project jobs page
442-
// Define URL to navigate to
443-
const jobsUrl = new URL(`projects/${project.id}/workflows/${workflow.id}/jobs`, window.location.origin);
444-
// Trigger navigation
445-
await goto(jobsUrl);
449+
await loadJobsStatus();
446450
} else {
447451
console.error(response);
448452
// Set an error message on the component
@@ -517,9 +521,46 @@
517521
async function updateNewVersionsCount(count) {
518522
newVersionsCount = count;
519523
}
524+
525+
/** @type {{[key: number]: import('$lib/types').JobStatus}} */
526+
let statuses = {};
527+
528+
/** @type {NodeJS.Timer|undefined} */
529+
let statusWatcherTimer;
530+
531+
async function loadJobsStatus() {
532+
if (selectedInputDatasetId === undefined || selectedOutputDatasetId === undefined) {
533+
return;
534+
}
535+
if (workflowErrorAlert) {
536+
workflowErrorAlert.hide();
537+
}
538+
const outputStatusResponse = await fetch(
539+
`/api/v1/project/${project.id}/dataset/${selectedOutputDatasetId}/status`,
540+
{
541+
method: 'GET',
542+
credentials: 'include'
543+
}
544+
);
545+
const outputStatus = await outputStatusResponse.json();
546+
if (!outputStatusResponse.ok) {
547+
workflowErrorAlert = displayStandardErrorAlert(outputStatus, 'workflowErrorAlert');
548+
return;
549+
}
550+
statuses = outputStatus.status;
551+
const runningOrSubmitted = Object.values(statuses).filter(s => s === 'running' || s === 'submitted');
552+
if (statusWatcherTimer) {
553+
if (runningOrSubmitted.length === 0) {
554+
clearTimeout(statusWatcherTimer);
555+
statusWatcherTimer = undefined;
556+
}
557+
} else if (runningOrSubmitted.length > 0) {
558+
statusWatcherTimer = setInterval(loadJobsStatus, updateJobsInterval);
559+
}
560+
}
520561
</script>
521562

522-
<div class="d-flex justify-content-between align-items-center mb-4">
563+
<div class="row">
523564
<nav aria-label="breadcrumb">
524565
<ol class="breadcrumb">
525566
<li class="breadcrumb-item" aria-current="page">
@@ -538,33 +579,76 @@
538579
{/if}
539580
</ol>
540581
</nav>
541-
<div>
542-
<a href="/projects/{project?.id}/workflows/{workflow?.id}/jobs" class="btn btn-light">
543-
<i class="bi-journal-code" /> List jobs
544-
</a>
545-
<button class="btn btn-light" on:click|preventDefault={handleExportWorkflow}>
546-
<i class="bi-box-arrow-up" />
547-
</button>
548-
<a id="downloadWorkflowButton" class="d-none">Download workflow link</a>
549-
<button
550-
class="btn btn-light"
551-
data-bs-toggle="modal"
552-
data-bs-target="#editWorkflowModal"
553-
on:click={resetWorkflowUpdateModal}
554-
>
555-
<i class="bi-gear-wide-connected" />
556-
</button>
557-
<button
558-
class="btn btn-success"
559-
on:click|preventDefault={() => {
560-
if (argumentsWithUnsavedChanges === false) {
561-
runWorkflowModal.toggle();
562-
} else {
563-
toggleUnsavedChangesModal();
564-
}
565-
}}
566-
><i class="bi-play-fill" /> Run workflow
567-
</button>
582+
</div>
583+
<div class="row mt-2">
584+
<div class="col-lg-9">
585+
<div class="row">
586+
<div class="col-lg-4 col-md-6">
587+
<div class="input-group mb-3">
588+
<label for="input-dataset" class="input-group-text">Input dataset</label>
589+
<select
590+
class="form-control"
591+
id="input-dataset"
592+
bind:value={selectedInputDatasetId}
593+
on:change={loadJobsStatus}
594+
>
595+
<option value={undefined}>Select...</option>
596+
{#each datasets as dataset}
597+
<option value={dataset.id}>{dataset.name}</option>
598+
{/each}
599+
</select>
600+
</div>
601+
</div>
602+
<div class="col-lg-4 col-md-6">
603+
<div class="input-group mb-3">
604+
<label for="output-dataset" class="input-group-text">Output dataset</label>
605+
<select
606+
class="form-control"
607+
id="output-dataset"
608+
bind:value={selectedOutputDatasetId}
609+
on:change={loadJobsStatus}
610+
>
611+
<option value={undefined}>Select...</option>
612+
{#each datasets as dataset}
613+
<option value={dataset.id}>{dataset.name}</option>
614+
{/each}
615+
</select>
616+
</div>
617+
</div>
618+
<div class="col-lg-4 col-md-12">
619+
<button
620+
class="btn btn-success"
621+
on:click|preventDefault={() => {
622+
if (argumentsWithUnsavedChanges === false) {
623+
runWorkflowModal.toggle();
624+
} else {
625+
toggleUnsavedChangesModal();
626+
}
627+
}}
628+
><i class="bi-play-fill" /> Run workflow
629+
</button>
630+
</div>
631+
</div>
632+
</div>
633+
634+
<div class="col-lg-3">
635+
<div class="float-end">
636+
<a href="/projects/{project?.id}/workflows/{workflow?.id}/jobs" class="btn btn-light">
637+
<i class="bi-journal-code" /> List jobs
638+
</a>
639+
<button class="btn btn-light" on:click|preventDefault={handleExportWorkflow}>
640+
<i class="bi-box-arrow-up" />
641+
</button>
642+
<a id="downloadWorkflowButton" class="d-none">Download workflow link</a>
643+
<button
644+
class="btn btn-light"
645+
data-bs-toggle="modal"
646+
data-bs-target="#editWorkflowModal"
647+
on:click={resetWorkflowUpdateModal}
648+
>
649+
<i class="bi-gear-wide-connected" />
650+
</button>
651+
</div>
568652
</div>
569653
</div>
570654

@@ -573,7 +657,7 @@
573657

574658
<div id="workflowErrorAlert" />
575659

576-
<div class="container mt-4 px-0">
660+
<div class="container mt-3 px-0">
577661
<div class="row">
578662
<div class="col-4">
579663
<div class="card">
@@ -616,6 +700,9 @@
616700
>
617701
{workflowTask.task.name}
618702

703+
<span class="float-end ps-2">
704+
<JobStatusIcon status={statuses[workflowTask.id]} />
705+
</span>
619706
{#if newVersionsMap[workflowTask.task.id]?.length > 0}
620707
<span class="float-end text-warning" title="new version available">
621708
<i class="bi bi-exclamation-triangle" />
@@ -951,9 +1038,9 @@
9511038
id="inputDataset"
9521039
class="form-control"
9531040
disabled={checkingConfiguration}
954-
bind:value={inputDatasetControl}
1041+
bind:value={selectedInputDatasetId}
9551042
>
956-
<option value="">Select an input dataset</option>
1043+
<option value={undefined}>Select an input dataset</option>
9571044
{#each datasets as dataset}
9581045
<option value={dataset.id}>{dataset.name}</option>
9591046
{/each}
@@ -966,9 +1053,9 @@
9661053
id="outputDataset"
9671054
class="form-control"
9681055
disabled={checkingConfiguration}
969-
bind:value={outputDatasetControl}
1056+
bind:value={selectedOutputDatasetId}
9701057
>
971-
<option value="">Select an output dataset</option>
1058+
<option value={undefined}>Select an output dataset</option>
9721059
{#each datasets as dataset}
9731060
<option value={dataset.id}>{dataset.name}</option>
9741061
{/each}

0 commit comments

Comments
 (0)