Skip to content

Commit d145cc7

Browse files
authored
Merge pull request #352 from fractal-analytics-platform/admin-jobs
Admin jobs page
2 parents 49219c5 + 901fc0b commit d145cc7

File tree

27 files changed

+940
-205
lines changed

27 files changed

+940
-205
lines changed

CHANGELOG.md

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

55
This release requires fractal-server 1.4.0.
66

7+
* Added admin jobs page (\#352).
8+
* Fixed expiration token issue for /admin and /auth endpoints (\#352).
79
* Used new endpoints for retrieving current user and list of users (\#350).
810
* Added user profile page (\#336).
911
* Added admin area with users management (\#336).

__tests__/JobsList.test.js

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -118,28 +118,6 @@ describe('JobsList', () => {
118118
await fireEvent.click(clearFiltersBtn);
119119
}
120120

121-
it('refresh jobs', async () => {
122-
const jobUpdater = function () {
123-
return data.jobs.map((j) => (j.status === 'running' ? { ...j, status: 'done' } : j));
124-
};
125-
const result = render(JobsList, {
126-
props: { jobUpdater }
127-
});
128-
let table = result.getByRole('table');
129-
expect(table.querySelectorAll('tbody tr').length).eq(3);
130-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('running');
131-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
132-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
133-
134-
const refreshButton = result.getByRole('button', { name: 'Refresh' });
135-
await fireEvent.click(refreshButton);
136-
137-
table = result.getByRole('table');
138-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('done');
139-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
140-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
141-
});
142-
143121
it('cancel job', async () => {
144122
const nop = function () {};
145123
const result = render(JobsList, {
@@ -190,19 +168,19 @@ describe('JobsList', () => {
190168
});
191169
let table = result.getByRole('table');
192170
expect(table.querySelectorAll('tbody tr').length).eq(3);
193-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('running');
194-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
195-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
171+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).contain('running');
172+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).contain('failed');
173+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).contain('done');
196174

197175
vi.advanceTimersByTime(3500);
198176
vi.useRealTimers();
199177
// trigger table update
200178
await new Promise(setTimeout);
201179

202180
table = result.getByRole('table');
203-
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).eq('done');
204-
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).eq('failed');
205-
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).eq('done');
181+
expect(table.querySelectorAll('tbody tr:nth-child(1) td')[0].textContent).contain('done');
182+
expect(table.querySelectorAll('tbody tr:nth-child(2) td')[0].textContent).contain('failed');
183+
expect(table.querySelectorAll('tbody tr:nth-child(3) td')[0].textContent).contain('done');
206184
} finally {
207185
vi.useRealTimers();
208186
}

__tests__/mock/jobs-list.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export const data = {
2+
userInfo: {
3+
4+
},
25
projects: [
36
{
47
id: 1,
@@ -38,7 +41,11 @@ export const data = {
3841
output_dataset_id: 2,
3942
start_timestamp: '2023-10-30T09:00:38.442196',
4043
end_timestamp: '2023-10-30T09:10:38.442196',
41-
status: 'done'
44+
workflow_dump: { id: 1, name: 'workflow 1' },
45+
input_dataset_dump: { id: 1, name: 'input1' },
46+
output_dataset_dump: { id: 2, name: 'output1' },
47+
status: 'done',
48+
user_email: '[email protected]'
4249
},
4350
{
4451
id: 2,
@@ -48,7 +55,11 @@ export const data = {
4855
output_dataset_id: 4,
4956
start_timestamp: '2023-10-30T09:15:38.442196',
5057
end_timestamp: '2023-10-30T09:20:38.442196',
51-
status: 'failed'
58+
workflow_dump: { id: 2, name: 'workflow 2' },
59+
input_dataset_dump: { id: 3, name: 'input2' },
60+
output_dataset_dump: { id: 4, name: 'output2' },
61+
status: 'failed',
62+
user_email: '[email protected]'
5263
},
5364
{
5465
id: 3,
@@ -57,8 +68,11 @@ export const data = {
5768
input_dataset_id: 5,
5869
output_dataset_id: 6,
5970
start_timestamp: '2023-10-30T09:30:38.442196',
71+
input_dataset_dump: { id: 5, name: 'input3' },
72+
output_dataset_dump: { id: 6, name: 'output3' },
6073
end_timestamp: null,
61-
status: 'running'
74+
status: 'running',
75+
user_email: '[email protected]'
6276
}
6377
]
6478
};

docs/development/structure.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,15 @@ The following image provides an overview for the reader of the described archite
8383

8484
To avoid duplicating the logic of each fractal-server endpoint and simplify the error handling, a special Svelte route has been setup to act like a transparent proxy: `src/routes/api/[...path]/+server.js`. This is one of the suggested way to handle a different backend [according to Svelte Kit FAQ](https://kit.svelte.dev/docs/faq#how-do-i-use-x-with-sveltekit-how-do-i-use-a-different-backend-api-server).
8585

86-
So, by default, the AJAX calls performed by the front-end have exactly the same path and payload of the fractal-server API, but are sent to the Node.js Svelte back-end.
86+
So, by default, the AJAX calls performed by the front-end have the same path and payload of the fractal-server API, but are sent to the Node.js Svelte back-end. Some Python API endpoints (like the `/auth` and `/admin` endpoints) that don't start with `/api` are handled in a slightly different way. Indeed, the `/api` prefix is needed by `hooks.server.js` to detect if the received call is an AJAX call, in order to process the token expiration in a custom way. To preserve this behavior for all the API calls these paths are rewritten adding the `/api` prefix.
8787

88-
Other than the AJAX calls, there are also some calls to fractal-server API done by Svelte SSR, while generating the HTML page. These requests are defined in files under `src/lib/server/api/v1`. Here requests are grouped by contexts as `auth_api`, `monitoring_api`, [...].
88+
Summarizing, the frontend code:
89+
90+
* uses exactly the same path of the fractal-server API for the `/api` endpoints
91+
* uses `/api/auth` for `/auth` endpoints
92+
* uses `/api/admin` for `/admin` endpoints
93+
94+
Other than the AJAX calls, there are also some calls to fractal-server API done by Svelte SSR, while generating the HTML page. These requests are defined in files under `src/lib/server/api/v1`. Here requests are grouped by contexts as `auth_api`, `admin_api`, [...].
8995

9096
### An example using actions
9197

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.0a8',
41+
command: './tests/start-test-server.sh 1.4.0a10',
4242
port: 8000,
4343
waitForPort: true,
4444
stdout: 'pipe',

src/lib/components/jobs/JobInfoModal.svelte

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import StatusBadge from '$lib/components/jobs/StatusBadge.svelte';
33
import { displayStandardErrorAlert } from '$lib/common/errors';
4+
import { page } from '$app/stores';
45
import Modal from '../common/Modal.svelte';
56
67
/** @type {{id: number, name: string}[]} */
@@ -37,9 +38,13 @@
3738
// Should update jobWorkflowName
3839
jobWorkflowName = workflows.find((workflow) => workflow.id === jobToDisplay.workflow_id)?.name;
3940
// Should update jobInputDatasetName
40-
jobInputDatasetName = datasets.find((dataset) => dataset.id === jobToDisplay.input_dataset_id)?.name;
41+
jobInputDatasetName = datasets.find(
42+
(dataset) => dataset.id === jobToDisplay.input_dataset_id
43+
)?.name;
4144
// Should update jobOutputDatasetName
42-
jobOutputDatasetName = datasets.find((dataset) => dataset.id === jobToDisplay.output_dataset_id)?.name;
45+
jobOutputDatasetName = datasets.find(
46+
(dataset) => dataset.id === jobToDisplay.output_dataset_id
47+
)?.name;
4348
// Should update jobStatus
4449
jobStatus = job.status;
4550
@@ -69,35 +74,39 @@
6974
<Modal id="workflowJobInfoModal" bind:this={modal} size="lg">
7075
<svelte:fragment slot="header">
7176
<h1 class="h5 modal-title flex-grow-1">Workflow Job #{workflowJobId}</h1>
72-
<button class="btn btn-light me-3" on:click={fetchJob}>
73-
<i class="bi-arrow-clockwise" />
74-
</button>
77+
{#if job && job.user_email === $page.data.userInfo.email && job.project_id !== null}
78+
<button class="btn btn-light me-3" on:click={fetchJob}>
79+
<i class="bi-arrow-clockwise" />
80+
</button>
81+
{/if}
7582
</svelte:fragment>
7683
<svelte:fragment slot="body">
7784
<div class="row mb-3">
7885
<div class="col-12">
7986
<div id="workflowJobError" />
8087
<p class="lead">Workflow job properties</p>
81-
<ul class="list-group">
82-
<li class="list-group-item list-group-item-light fw-bold">Id</li>
83-
<li class="list-group-item">{job?.id}</li>
84-
<li class="list-group-item list-group-item-light fw-bold">Workflow</li>
85-
<li class="list-group-item">{jobWorkflowName}</li>
86-
<li class="list-group-item list-group-item-light fw-bold">Project</li>
87-
<li class="list-group-item">{projectName}</li>
88-
<li class="list-group-item list-group-item-light fw-bold">Input dataset</li>
89-
<li class="list-group-item">{jobInputDatasetName}</li>
90-
<li class="list-group-item list-group-item-light fw-bold">Output dataset</li>
91-
<li class="list-group-item">{jobOutputDatasetName}</li>
92-
<li class="list-group-item list-group-item-light fw-bold">Status</li>
93-
{#key jobStatus}
94-
<li class="list-group-item"><StatusBadge status={jobStatus} /></li>
95-
{/key}
96-
<li class="list-group-item list-group-item-light fw-bold">Working directory</li>
97-
<li class="list-group-item"><code>{job?.working_dir}</code></li>
98-
<li class="list-group-item list-group-item-light fw-bold">User Working directory</li>
99-
<li class="list-group-item"><code>{job?.working_dir_user}</code></li>
100-
</ul>
88+
{#if job}
89+
<ul class="list-group">
90+
<li class="list-group-item list-group-item-light fw-bold">Id</li>
91+
<li class="list-group-item">{job.id}</li>
92+
<li class="list-group-item list-group-item-light fw-bold">Workflow</li>
93+
<li class="list-group-item">{job.workflow_dump?.name || '-'}</li>
94+
<li class="list-group-item list-group-item-light fw-bold">Project</li>
95+
<li class="list-group-item">{projectName || '-'}</li>
96+
<li class="list-group-item list-group-item-light fw-bold">Input dataset</li>
97+
<li class="list-group-item">{job.input_dataset_dump?.name || '-'}</li>
98+
<li class="list-group-item list-group-item-light fw-bold">Output dataset</li>
99+
<li class="list-group-item">{job.output_dataset_dump?.name || '-'}</li>
100+
<li class="list-group-item list-group-item-light fw-bold">Status</li>
101+
{#key jobStatus}
102+
<li class="list-group-item"><StatusBadge status={jobStatus} /></li>
103+
{/key}
104+
<li class="list-group-item list-group-item-light fw-bold">Working directory</li>
105+
<li class="list-group-item"><code>{job.working_dir}</code></li>
106+
<li class="list-group-item list-group-item-light fw-bold">User Working directory</li>
107+
<li class="list-group-item"><code>{job.working_dir_user}</code></li>
108+
</ul>
109+
{/if}
101110
</div>
102111
</div>
103112
<div class="row">

src/lib/components/jobs/JobLogsModal.svelte

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
import { displayStandardErrorAlert } from '$lib/common/errors';
33
import Modal from '../common/Modal.svelte';
44
5-
/** @type {number|undefined} */
6-
let projectId = undefined;
7-
let workflowJobId = undefined;
85
let logs = '';
96
let errorAlert = undefined;
107
/** @type {Modal} */
@@ -13,23 +10,30 @@
1310
/**
1411
* @param prjId {number}
1512
* @param jobId {number}
13+
* @param log {string|null=}
1614
*/
17-
export async function show(prjId, jobId) {
18-
projectId = prjId;
19-
workflowJobId = jobId;
15+
export async function show(prjId, jobId, log) {
2016
2117
// remove previous error
2218
if (errorAlert) {
2319
errorAlert.hide();
2420
}
2521
26-
const job = await fetchJob();
27-
logs = job?.log || '';
22+
if (log) {
23+
logs = log;
24+
} else {
25+
const job = await fetchJob(prjId, jobId);
26+
logs = job?.log || '';
27+
}
2828
2929
modal.show();
3030
}
3131
32-
async function fetchJob() {
32+
/**
33+
* @param projectId {number}
34+
* @param workflowJobId {number}
35+
*/
36+
async function fetchJob(projectId, workflowJobId) {
3337
const request = await fetch(`/api/v1/project/${projectId}/job/${workflowJobId}`, {
3438
method: 'GET',
3539
credentials: 'include'

0 commit comments

Comments
 (0)