Skip to content

Commit 078321a

Browse files
authored
Merge pull request #363 from fractal-analytics-platform/workflow-monitoring
Monitoring of job status in workflow page
2 parents 85388fe + 57dd079 commit 078321a

File tree

10 files changed

+1384
-23
lines changed

10 files changed

+1384
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
*Note: Numbers like (\#123) point to closed Pull Requests on the fractal-web repository.*
22

3+
# Unreleased
4+
5+
* Added experimental workflow page with job monitoring (\#363).
6+
37
# 0.7.0
48

59
This release requires fractal-server 1.4.0.

playwright.config.js

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

3939
webServer: [
4040
{
41-
command: './tests/start-test-server.sh 1.4.0a10',
41+
command: './tests/start-test-server.sh 1.4.0',
4242
port: 8000,
4343
waitForPort: true,
4444
stdout: 'pipe',

src/lib/components/common/jschema/ArrayProperty.svelte

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,28 @@
2929
3030
{#if schemaProperty}
3131
<div class="d-flex flex-column p-2">
32-
<div class="property-metadata d-flex flex-row w-100">
33-
<span class={schemaProperty.isRequired() ? 'fw-bold' : ''}>{schemaProperty.title || ''}</span>
34-
<PropertyDescription description={schemaProperty.description} />
35-
</div>
3632
<div class="array-items my-2">
3733
<div class="accordion" id={accordionParentKey}>
3834
<div class="accordion-item">
3935
<div class="accordion-header">
4036
<button
41-
class="accordion-button collapsed"
37+
class="accordion-button"
38+
class:collapsed={!schemaProperty.isRequired()}
4239
type="button"
4340
data-bs-toggle="collapse"
4441
data-bs-target="#{collapseSymbol}"
4542
>
46-
Arguments list
43+
<span class={schemaProperty.isRequired() ? 'fw-bold' : ''}>
44+
{schemaProperty.title || ''}
45+
</span>
46+
<PropertyDescription description={schemaProperty.description} />
4747
</button>
4848
</div>
4949
<div
5050
id={collapseSymbol}
51-
class="accordion-collapse collapse"
51+
class="accordion-collapse"
52+
class:collapse={!schemaProperty.isRequired()}
53+
class:show={schemaProperty.isRequired()}
5254
data-bs-parent="#{accordionParentKey}"
5355
>
5456
<div class="accordion-body p-1">

src/lib/components/common/jschema/ObjectProperty.svelte

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,26 @@
3838

3939
{#if objectSchema}
4040
<div class="d-flex flex-column p-2">
41-
<div class="property-metadata d-flex flex-row w-100">
42-
<span class={objectSchema.isRequired() ? 'fw-bold' : ''}>{objectSchema.title}</span>
43-
<PropertyDescription description={objectSchema.description} />
44-
</div>
4541
<div class="object-properties my-2">
4642
<div class="accordion" id={accordionParentKey}>
4743
<div class="accordion-item">
4844
<div class="accordion-header">
4945
<button
50-
class="accordion-button collapsed"
46+
class="accordion-button"
47+
class:collapsed={!objectSchema.isRequired()}
5148
type="button"
5249
data-bs-toggle="collapse"
5350
data-bs-target="#{collapseSymbol}"
5451
>
55-
Argument Properties
52+
<span class={objectSchema.isRequired() ? 'fw-bold' : ''}>{objectSchema.title}</span>
53+
<PropertyDescription description={objectSchema.description} />
5654
</button>
5755
</div>
5856
<div
5957
id={collapseSymbol}
60-
class="accordion-collapse collapse"
58+
class="accordion-collapse"
59+
class:collapse={!objectSchema.isRequired()}
60+
class:show={objectSchema.isRequired()}
6161
data-bs-parent="#{accordionParentKey}"
6262
>
6363
<div class="accordion-body p-1">

src/lib/components/common/jschema/PropertyDescription.svelte

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,34 @@
33
44
export let description = undefined;
55
6+
/** @type {HTMLElement|undefined} */
67
let element;
78
9+
let popover;
10+
let windowEventListener;
11+
812
onMount(() => {
913
if (element) {
10-
// @ts-ignore
11-
// eslint-disable-next-line no-undef
12-
new bootstrap.Popover(element);
14+
element.addEventListener('click', function () {
15+
// @ts-ignore
16+
// eslint-disable-next-line no-undef
17+
popover = bootstrap.Popover.getOrCreateInstance(element, { trigger: 'manual' });
18+
popover.toggle();
19+
if (!windowEventListener) {
20+
windowEventListener = function (/** @type {MouseEvent} */ event) {
21+
if (event.target instanceof HTMLElement && event.target !== element) {
22+
const clickedPopover = event.target.closest('.popover');
23+
if (!clickedPopover) {
24+
popover.hide();
25+
popover = undefined;
26+
window.removeEventListener('click', windowEventListener);
27+
windowEventListener = undefined;
28+
}
29+
}
30+
};
31+
window.addEventListener('click', windowEventListener);
32+
}
33+
});
1334
}
1435
});
1536
</script>
@@ -22,7 +43,8 @@
2243
role="button"
2344
data-bs-trigger="focus"
2445
class="bi bi-info-circle text-primary"
25-
data-bs-toggle="popover"
46+
data-bs-toggle="collapse"
47+
data-bs-target
2648
data-bs-content={description}
2749
/>
2850
{/if}
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/lib/components/jobs/JobsList.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,15 @@
121121
const updateJobsInterval = env.PUBLIC_UPDATE_JOBS_INTERVAL
122122
? parseInt(env.PUBLIC_UPDATE_JOBS_INTERVAL)
123123
: 3000;
124-
let updateJobsTimeout = null;
124+
let updateJobsTimeout = undefined;
125125
126126
async function updateJobsInBackground() {
127127
const jobsToCheck = jobs.filter((j) => j.status === 'running' || j.status === 'submitted');
128128
if (jobsToCheck.length > 0) {
129129
jobs = await jobUpdater();
130130
tableHandler.setRows(jobs);
131131
}
132+
clearTimeout(updateJobsTimeout);
132133
updateJobsTimeout = setTimeout(updateJobsInBackground, updateJobsInterval);
133134
}
134135
@@ -137,9 +138,7 @@
137138
});
138139
139140
onDestroy(() => {
140-
if (updateJobsTimeout) {
141-
clearTimeout(updateJobsTimeout);
142-
}
141+
clearTimeout(updateJobsTimeout);
143142
});
144143
</script>
145144

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<script>
2+
import Modal from '../common/Modal.svelte';
3+
4+
/** @type {number} */
5+
export let projectId;
6+
/** @type {import('$lib/types').Workflow} */
7+
export let workflow;
8+
9+
/** @type {(workflow: import('$lib/types').Workflow) => void} */
10+
export let workflowUpdater;
11+
12+
/** @type {Modal} */
13+
let editWorkflowTasksOrderModal;
14+
15+
/** @type {{id: number, name: string}[]} */
16+
let editableTasksList = [];
17+
18+
/**
19+
* @param {import('$lib/types').WorkflowTask[]} originalTasksList
20+
*/
21+
export function show(originalTasksList) {
22+
editableTasksList = originalTasksList.map((task) => ({ id: task.id, name: task.task.name }));
23+
editWorkflowTasksOrderModal.show();
24+
}
25+
26+
/**
27+
* @param {number} index
28+
* @param {'up'|'down'} direction
29+
*/
30+
function moveWorkflowTask(index, direction) {
31+
const wftList = editableTasksList;
32+
33+
let replaceId;
34+
switch (direction) {
35+
case 'up':
36+
if (index === 0) break;
37+
replaceId = index - 1;
38+
break;
39+
case 'down':
40+
if (index === wftList.length - 1) break;
41+
replaceId = index + 1;
42+
}
43+
44+
if (replaceId !== undefined) {
45+
const replaceTask = wftList[replaceId];
46+
wftList[replaceId] = wftList[index];
47+
wftList[index] = replaceTask;
48+
editableTasksList = wftList;
49+
}
50+
}
51+
52+
/**
53+
* Reorders a project's workflow in the server
54+
* @returns {Promise<*>}
55+
*/
56+
async function handleWorkflowOrderUpdate() {
57+
if (!workflow) {
58+
return;
59+
}
60+
const patchData = {
61+
reordered_workflowtask_ids: editableTasksList.map((t) => t.id)
62+
};
63+
64+
const headers = new Headers();
65+
headers.set('Content-Type', 'application/json');
66+
67+
const response = await fetch(`/api/v1/project/${projectId}/workflow/${workflow.id}`, {
68+
method: 'PATCH',
69+
credentials: 'include',
70+
mode: 'cors',
71+
headers,
72+
body: JSON.stringify(patchData)
73+
});
74+
75+
const result = await response.json();
76+
if (response.ok) {
77+
console.log('Workflow task order updated');
78+
workflowUpdater(result);
79+
editWorkflowTasksOrderModal.toggle();
80+
} else {
81+
console.error('Workflow task order not updated', result);
82+
editWorkflowTasksOrderModal.displayErrorAlert(result);
83+
}
84+
}
85+
</script>
86+
87+
<Modal id="editWorkflowTasksOrderModal" centered={true} bind:this={editWorkflowTasksOrderModal}>
88+
<svelte:fragment slot="header">
89+
<h5 class="modal-title">Edit workflow tasks order</h5>
90+
</svelte:fragment>
91+
<svelte:fragment slot="body">
92+
<div id="errorAlert-editWorkflowTasksOrderModal" />
93+
{#if workflow !== undefined && editableTasksList.length == 0}
94+
<p class="text-center mt-3">No workflow tasks yet, add one.</p>
95+
{:else if workflow !== undefined}
96+
{#key editableTasksList}
97+
<ul class="list-group list-group-flush">
98+
{#each editableTasksList as workflowTask, i}
99+
<li class="list-group-item" data-fs-target={workflowTask.id}>
100+
<div class="d-flex justify-content-between align-items-center">
101+
<div>
102+
{workflowTask.name} #{workflowTask.id}
103+
</div>
104+
<div>
105+
{#if i !== 0}
106+
<button
107+
class="btn btn-light"
108+
on:click|preventDefault={() => moveWorkflowTask(i, 'up')}
109+
>
110+
<i class="bi-arrow-up" />
111+
</button>
112+
{/if}
113+
{#if i !== editableTasksList.length - 1}
114+
<button
115+
class="btn btn-light"
116+
on:click|preventDefault={() => moveWorkflowTask(i, 'down')}
117+
>
118+
<i class="bi-arrow-down" />
119+
</button>
120+
{/if}
121+
</div>
122+
</div>
123+
</li>
124+
{/each}
125+
</ul>
126+
{/key}
127+
{/if}
128+
</svelte:fragment>
129+
<svelte:fragment slot="footer">
130+
<button class="btn btn-primary" on:click|preventDefault={handleWorkflowOrderUpdate}>
131+
Save
132+
</button>
133+
</svelte:fragment>
134+
</Modal>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getWorkflow } from '$lib/server/api/v1/workflow_api';
2+
import { getProject, getProjectDatasets } from '$lib/server/api/v1/project_api';
3+
4+
export async function load({ fetch, params }) {
5+
console.log('Load workflow page');
6+
7+
const { projectId, workflowId } = params;
8+
9+
// Get the project
10+
const project = await getProject(fetch, projectId);
11+
12+
// Get the workflow
13+
const workflow = await getWorkflow(fetch, projectId, workflowId);
14+
15+
// Get the datasets
16+
const datasets = await getProjectDatasets(fetch, projectId);
17+
18+
return {
19+
project,
20+
workflow,
21+
datasets
22+
};
23+
}

0 commit comments

Comments
 (0)