Skip to content

Commit cfb021f

Browse files
authored
Merge pull request #484 from fractal-analytics-platform/tasks-admin-page
Implemented tasks admin page
2 parents 54f3b32 + 06b24d2 commit cfb021f

File tree

20 files changed

+948
-187
lines changed

20 files changed

+948
-187
lines changed

.github/workflows/end_to_end_tests.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ jobs:
1616
matrix:
1717
node-version: ['16', '18', '20']
1818

19+
services:
20+
postgres:
21+
image: postgres
22+
env:
23+
POSTGRES_PASSWORD: postgres
24+
POSTGRES_DB: fractal_test
25+
options: >-
26+
--health-cmd pg_isready
27+
--health-interval 10s
28+
--health-timeout 5s
29+
--health-retries 5
30+
ports:
31+
- 5432:5432
32+
1933
steps:
2034
- name: Check out repo
2135
uses: actions/checkout@v4
@@ -39,4 +53,4 @@ jobs:
3953
run: echo "FRACTAL_SERVER_HOST=http://127.0.0.1:8000" >> $GITHUB_ENV
4054

4155
- name: Run Playwright tests
42-
run: npx playwright test --workers=1
56+
run: npx playwright test

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
# Unreleased
44

5+
* Setup tests to use PostgreSQL instead of SQlite (\#484).
6+
* Implemented tasks admin page (\#484).
57
* Improved form builder used in workflow tasks without JSON Schema and in Meta properties tab (\#481).
68
* Used collapsible sections in dataset history modal (\#481).
79

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { onMount } from 'svelte';
33
44
export let description = undefined;
5+
export let html = false;
56
67
/** @type {HTMLElement|undefined} */
78
let element;
@@ -45,6 +46,7 @@
4546
class="bi bi-info-circle text-primary"
4647
data-bs-toggle="collapse"
4748
data-bs-target
49+
data-bs-html={html}
4850
data-bs-content={description}
4951
/>
5052
{/if}

src/lib/types-v2.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,31 @@ export type WorkflowTaskV2 = {
125125
task_legacy_id: number
126126
task_legacy: Task
127127
})
128+
129+
type TaskV2Minimal = {
130+
id: number
131+
name: string
132+
type: string
133+
command_non_parallel: string | null
134+
command_parallel: string | null
135+
source: string
136+
source: string | null
137+
owner: string | null
138+
version: string | null
139+
}
140+
141+
type TaskV2Relationship = {
142+
workflow_id: number
143+
workflow_name: string
144+
project_id: number
145+
project_name: string
146+
project_users: Array<{
147+
id: number
148+
email: string
149+
}> | null
150+
}
151+
152+
export type TaskV2Info = {
153+
task: TaskV2Minimal,
154+
relationships: Array<TaskV2Relationship>
155+
}

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
<a href="/v2/admin/users" class="btn btn-primary mt-3">
2-
<i class="bi bi-people-fill" />
3-
Manage users
4-
</a>
1+
<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">
4+
<i class="bi bi-people-fill" />
5+
Manage users
6+
</a>
57

6-
<br />
8+
<br />
79

8-
<a href="/v2/admin/jobs" class="btn btn-primary mt-3">
9-
<i class="bi bi-gear-fill" />
10-
Jobs
11-
</a>
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>
1214

13-
<br />
15+
<br />
1416

15-
<a href="/v2/admin/tasks" class="btn btn-primary mt-3">
16-
<i class="bi bi-list-task" />
17-
Tasks V1/V2 compatibility
18-
</a>
17+
<a href="/v2/admin/tasks" class="btn btn-primary mt-3 w-100">
18+
<i class="bi bi-list-task" />
19+
Tasks
20+
</a>
21+
22+
<br />
23+
24+
<a href="/v2/admin/tasks-compatibility" class="btn btn-primary mt-3 w-100">
25+
<i class="bi bi-ui-checks" />
26+
Tasks V1/V2 compatibility
27+
</a>
28+
</div>
29+
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { orderTasksByOwnerThenByNameThenByVersion } from '$lib/common/component_utilities';
2+
import { listLegacyTasks } from '$lib/server/api/v2/task_api.js';
3+
4+
export async function load({ fetch }) {
5+
/** @type {import('$lib/types').Task[]} */
6+
const tasks = await listLegacyTasks(fetch);
7+
8+
orderTasksByOwnerThenByNameThenByVersion(tasks, null, 'desc');
9+
10+
return {
11+
tasks
12+
};
13+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script>
2+
import { page } from '$app/stores';
3+
import { displayStandardErrorAlert } from '$lib/common/errors';
4+
import TasksTable from '$lib/components/tasks/TasksTable.svelte';
5+
6+
/** @type {import('$lib/components/common/StandardErrorAlert.svelte').default|undefined} */
7+
let errorAlert;
8+
9+
/** @type {import('$lib/types').Task[]} */
10+
let tasks = $page.data.tasks;
11+
12+
/**
13+
* Instead of replacing the whole tasks object on successful PATCH response, this compatibility map
14+
* is used to draw the state of checkboxes. This prevents the complete redraw of the table when the
15+
* compatibility of a single task change, avoiding to collapse the opened old versions rows.
16+
* @type {{[taskId: string]: boolean}}
17+
*/
18+
let compatibilities = Object.fromEntries(tasks.map((t) => [t.id, t.is_v2_compatible]));
19+
20+
/** @type {number|null} */
21+
let editingTaskId = null;
22+
23+
/**
24+
* @param {Event} event
25+
* @param {import('$lib/types').Task} task
26+
*/
27+
async function setV2Compatible(event, task) {
28+
if (errorAlert) {
29+
errorAlert.hide();
30+
}
31+
editingTaskId = task.id;
32+
const headers = new Headers();
33+
headers.set('Content-Type', 'application/json');
34+
const is_v2_compatible = !task.is_v2_compatible;
35+
const response = await fetch(`/api/admin/v2/task-v1/${task.id}`, {
36+
method: 'PATCH',
37+
headers,
38+
body: JSON.stringify({ is_v2_compatible })
39+
});
40+
if (response.ok) {
41+
// Update compatibilities map and task object
42+
compatibilities[task.id] = is_v2_compatible;
43+
task.is_v2_compatible = is_v2_compatible;
44+
compatibilities = compatibilities;
45+
} else {
46+
errorAlert = displayStandardErrorAlert(
47+
await response.json(),
48+
`errorAlert-tasksCompatibility`
49+
);
50+
// reset input state in case of error
51+
if (event.target instanceof HTMLInputElement) {
52+
event.target.checked = !is_v2_compatible;
53+
}
54+
}
55+
editingTaskId = null;
56+
}
57+
</script>
58+
59+
<div>
60+
<div class="d-flex justify-content-between align-items-center mb-3">
61+
<h1 class="fw-light">v1/v2 tasks compatibility</h1>
62+
</div>
63+
</div>
64+
65+
<div id="errorAlert-tasksCompatibility" />
66+
67+
<TasksTable {tasks}>
68+
<svelte:fragment slot="thead">
69+
<thead>
70+
<tr>
71+
<th>Id</th>
72+
<th>Name</th>
73+
<th>Version</th>
74+
<th>V2 compatible</th>
75+
</tr>
76+
</thead>
77+
</svelte:fragment>
78+
<svelte:fragment slot="custom-columns-left" let:task>
79+
<td>{task.id}</td>
80+
</svelte:fragment>
81+
<svelte:fragment slot="custom-columns-right" let:task>
82+
<td>
83+
<div class="form-check form-switch">
84+
<input
85+
class="form-check-input"
86+
type="checkbox"
87+
role="switch"
88+
id="task-{task.id}-compatible"
89+
on:change={(event) => setV2Compatible(event, task)}
90+
checked={compatibilities[task.id]}
91+
disabled={editingTaskId === task.id}
92+
/>
93+
<label class="form-check-label" for="task-{task.id}-compatible">
94+
{compatibilities[task.id]}
95+
{#if editingTaskId === task.id}
96+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
97+
{/if}
98+
</label>
99+
</div>
100+
</td>
101+
</svelte:fragment>
102+
</TasksTable>
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { orderTasksByOwnerThenByNameThenByVersion } from '$lib/common/component_utilities';
2-
import { listLegacyTasks } from '$lib/server/api/v2/task_api.js';
1+
import { listUsers } from '$lib/server/api/v1/auth_api.js';
32

43
export async function load({ fetch }) {
5-
/** @type {import('$lib/types').Task[]} */
6-
const tasks = await listLegacyTasks(fetch);
4+
/** @type {import('$lib/types').User[]} */
5+
const usersList = await listUsers(fetch);
76

8-
orderTasksByOwnerThenByNameThenByVersion(tasks, null, 'desc');
7+
const users = /** @type {string[]} */ (
8+
usersList.map((u) => (u.username ? u.username : u.slurm_user)).filter((u) => !!u)
9+
);
10+
11+
users.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
912

1013
return {
11-
tasks
14+
users
1215
};
1316
}

0 commit comments

Comments
 (0)