Skip to content

Commit d8647e3

Browse files
committed
Added admin page for job submission healthcheck
1 parent bcdc425 commit d8647e3

File tree

5 files changed

+279
-13
lines changed

5 files changed

+279
-13
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 admin page for job submission healthcheck (\#543);
6+
37
# 1.4.1
48

59
* Raised a warning upon import/export of a workflow with a custom task (\#542);

src/lib/components/common/StandardErrorAlert.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
{#if errorString}
4747
<div class="alert alert-danger alert-dismissible" role="alert">
48+
<slot />
4849
{#if formatAsPre}
4950
<p>There has been an error, reason:</p>
5051
<pre>{errorString}</pre>

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

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
11
<div class="row">
2-
<div class="col-xl-3 col-lg-4 col-md-5">
3-
<a href="/v2/admin/users" class="btn btn-primary mt-3 w-100">
2+
<div class="col">
3+
4+
<h2 class="fw-light">Users</h2>
5+
6+
<a href="/v2/admin/users" class="btn btn-primary">
47
<i class="bi bi-people-fill" />
58
Manage users
69
</a>
710

8-
<br />
11+
<h2 class="fw-light mt-3">Jobs</h2>
912

10-
<a href="/v2/admin/jobs" class="btn btn-primary mt-3 w-100">
11-
<i class="bi bi-gear-fill" />
12-
Jobs
13+
<a href="/v2/admin/jobs" class="btn btn-primary me-2">
14+
<i class="bi bi-search" />
15+
Search jobs
1316
</a>
1417

15-
<br />
16-
17-
<a href="/v2/admin/tasks" class="btn btn-primary mt-3 w-100">
18-
<i class="bi bi-list-task" />
19-
Tasks
18+
<a href="/v2/admin/jobs/healthcheck" class="btn btn-primary">
19+
<i class="bi bi-heart-pulse-fill"></i>
20+
Job submission healthcheck
2021
</a>
2122

22-
<br />
23+
<h2 class="fw-light mt-3">Tasks</h2>
24+
25+
<a href="/v2/admin/tasks" class="btn btn-primary me-2">
26+
<i class="bi bi-search" />
27+
Search tasks
28+
</a>
2329

24-
<a href="/v2/admin/tasks-compatibility" class="btn btn-primary mt-3 w-100">
30+
<a href="/v2/admin/tasks-compatibility" class="btn btn-primary">
2531
<i class="bi bi-ui-checks" />
2632
Tasks V1/V2 compatibility
2733
</a>
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<script>
2+
import { goto } from '$app/navigation';
3+
import { page } from '$app/stores';
4+
import { AlertError } from '$lib/common/errors';
5+
import StandardErrorAlert from '$lib/components/common/StandardErrorAlert.svelte';
6+
7+
let zarrDir = '';
8+
let inProgress = false;
9+
let stepMessage = '';
10+
let invalidZarrDir = false;
11+
let error = undefined;
12+
13+
async function startTest() {
14+
error = undefined;
15+
stepMessage = '';
16+
if (!zarrDir) {
17+
invalidZarrDir = true;
18+
return;
19+
}
20+
invalidZarrDir = false;
21+
inProgress = true;
22+
try {
23+
const projectId = await createProject();
24+
const datasetId = await createDataset(projectId);
25+
const workflowId = await createWorkflow(projectId);
26+
const taskId = await createHealthCheckTaskIfNeeded();
27+
await addTaskToWorkflow(projectId, workflowId, taskId);
28+
await submitWorkflow(projectId, workflowId, datasetId);
29+
await goto(`/v2/projects/${projectId}/workflows/${workflowId}`);
30+
} catch (err) {
31+
error = err;
32+
} finally {
33+
inProgress = false;
34+
}
35+
}
36+
37+
const headers = new Headers();
38+
headers.set('Content-Type', 'application/json');
39+
40+
async function createProject() {
41+
const randomPart = new Date().getTime();
42+
const userInfo = $page.data.userInfo;
43+
const projectName = `test_${userInfo.username || userInfo.id}_${randomPart}`;
44+
45+
stepMessage = `Creating project ${projectName}`;
46+
47+
const response = await fetch(`/api/v2/project`, {
48+
method: 'POST',
49+
credentials: 'include',
50+
headers,
51+
body: JSON.stringify({
52+
name: projectName
53+
})
54+
});
55+
56+
const result = await response.json();
57+
if (!response.ok) {
58+
throw new AlertError(result);
59+
}
60+
return result.id;
61+
}
62+
63+
/**
64+
* @param {number} projectId
65+
*/
66+
async function createDataset(projectId) {
67+
stepMessage = `Creating test dataset`;
68+
69+
const response = await fetch(`/api/v2/project/${projectId}/dataset`, {
70+
method: 'POST',
71+
credentials: 'include',
72+
headers,
73+
body: JSON.stringify({
74+
name: 'test',
75+
zarr_dir: zarrDir,
76+
filters: { attributes: {}, types: {} }
77+
})
78+
});
79+
80+
const result = await response.json();
81+
if (!response.ok) {
82+
throw new AlertError(result);
83+
}
84+
return result.id;
85+
}
86+
87+
/**
88+
* @param {number} projectId
89+
*/
90+
async function createWorkflow(projectId) {
91+
stepMessage = `Creating test workflow`;
92+
93+
const response = await fetch(`/api/v2/project/${projectId}/workflow`, {
94+
method: 'POST',
95+
credentials: 'include',
96+
headers,
97+
body: JSON.stringify({ name: 'test' })
98+
});
99+
100+
const result = await response.json();
101+
if (!response.ok) {
102+
throw new AlertError(result);
103+
}
104+
return result.id;
105+
}
106+
107+
async function createHealthCheckTaskIfNeeded() {
108+
stepMessage = `Checking if health check test task exists`;
109+
const taskId = await getHealthCheckTask();
110+
if (taskId !== undefined) {
111+
return taskId;
112+
}
113+
114+
stepMessage = `Creating health check test task`;
115+
116+
const response = await fetch(`/api/v2/task`, {
117+
method: 'POST',
118+
credentials: 'include',
119+
headers,
120+
body: JSON.stringify({
121+
name: 'job_submission_health_check',
122+
command_non_parallel: 'echo',
123+
source: 'job_submission_health_check',
124+
input_types: {},
125+
output_types: {}
126+
})
127+
});
128+
129+
const result = await response.json();
130+
if (!response.ok) {
131+
throw new AlertError(result);
132+
}
133+
return result.id;
134+
}
135+
136+
async function getHealthCheckTask() {
137+
const response = await fetch(`/api/v2/task`, {
138+
method: 'GET',
139+
credentials: 'include'
140+
});
141+
const result = await response.json();
142+
if (!response.ok) {
143+
throw new AlertError(result);
144+
}
145+
const tasks = result.filter((t) => t.source.endsWith(':job_submission_health_check'));
146+
return tasks.length > 0 ? tasks[0].id : undefined;
147+
}
148+
149+
/**
150+
* @param {number} projectId
151+
* @param {number} workflowId
152+
* @param {number} taskId
153+
*/
154+
async function addTaskToWorkflow(projectId, workflowId, taskId) {
155+
stepMessage = `Adding task to workflow`;
156+
157+
const response = await fetch(
158+
`/api/v2/project/${projectId}/workflow/${workflowId}/wftask?task_id=${taskId}`,
159+
{
160+
method: 'POST',
161+
credentials: 'include',
162+
headers,
163+
body: JSON.stringify({ is_legacy_task: false })
164+
}
165+
);
166+
const result = await response.json();
167+
if (!response.ok) {
168+
throw new AlertError(result);
169+
}
170+
}
171+
172+
/**
173+
* @param {number} projectId
174+
* @param {number} workflowId
175+
* @param {number} datasetId
176+
*/
177+
async function submitWorkflow(projectId, workflowId, datasetId) {
178+
stepMessage = `Submitting workflow`;
179+
180+
const response = await fetch(
181+
`/api/v2/project/${projectId}/job/submit?workflow_id=${workflowId}&dataset_id=${datasetId}`,
182+
{
183+
method: 'POST',
184+
credentials: 'include',
185+
headers,
186+
body: JSON.stringify({ first_task_index: 0 })
187+
}
188+
);
189+
const result = await response.json();
190+
if (!response.ok) {
191+
throw new AlertError(result);
192+
}
193+
}
194+
</script>
195+
196+
<div>
197+
<h1 class="fw-light mb-3">Job submission healthcheck</h1>
198+
199+
<div class="row">
200+
<div class="col">
201+
<div class="input-group mb-3" class:has-validation={invalidZarrDir}>
202+
<label class="input-group-text" for="zarrDir">Zarr directory</label>
203+
<input
204+
type="text"
205+
class="form-control"
206+
id="zarrDir"
207+
bind:value={zarrDir}
208+
class:is-invalid={invalidZarrDir}
209+
/>
210+
{#if invalidZarrDir}
211+
<div class="invalid-feedback">Zarr directory is required</div>
212+
{/if}
213+
</div>
214+
</div>
215+
</div>
216+
217+
<StandardErrorAlert {error}>
218+
<p>An error happened while executing the following step: <strong>{stepMessage}</strong></p>
219+
</StandardErrorAlert>
220+
221+
<div class="row">
222+
<div class="col">
223+
<button class="btn btn-primary" disabled={inProgress} on:click={startTest}>
224+
{#if inProgress}
225+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
226+
{/if}
227+
Test
228+
</button>
229+
</div>
230+
</div>
231+
</div>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitPageLoading } from '../utils.js';
3+
4+
test('Execute the admin job healthcheck', async ({ page }) => {
5+
await test.step('Open the admin job healthcheck page', async () => {
6+
await page.goto('/v2/admin/jobs/healthcheck');
7+
await waitPageLoading(page);
8+
});
9+
10+
await test.step('Attempt to run without setting the Zarr directory', async () => {
11+
await page.getByRole('button', { name: 'Test' }).click();
12+
await expect(page.getByText('Zarr directory is required')).toBeVisible();
13+
});
14+
15+
await test.step('Set zarr directory and start test', async () => {
16+
await page.getByRole('textbox', { name: 'Zarr directory' }).fill('/tmp');
17+
await page.getByRole('button', { name: 'Test' }).click();
18+
});
19+
20+
await test.step('Verify that is correctly redirected to the workflow page', async () => {
21+
await page.waitForURL(/\/v2\/projects\/\d+\/workflows\/\d+/);
22+
await expect(page.getByText('job_submission_health_check').first()).toBeVisible();
23+
});
24+
});

0 commit comments

Comments
 (0)