Skip to content

Commit 6138e5e

Browse files
authored
Merge pull request #543 from fractal-analytics-platform/admin-job-healthcheck
Added admin page for job submission healthcheck
2 parents bcdc425 + bb881ac commit 6138e5e

File tree

5 files changed

+295
-13
lines changed

5 files changed

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