Skip to content

Commit ae06a0f

Browse files
authored
Merge pull request #542 from fractal-analytics-platform/summer_improvements
Summer improvements
2 parents 67f3612 + f312624 commit ae06a0f

39 files changed

+964
-497
lines changed

.github/workflows/end_to_end_tests.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ jobs:
1212
runs-on: ubuntu-22.04
1313
timeout-minutes: 20
1414

15+
env:
16+
OAUTH_DEXIDP_CLIENT_ID: client_test_web_id
17+
OAUTH_DEXIDP_CLIENT_SECRET: client_test_web_secret
18+
OAUTH_DEXIDP_REDIRECT_URL: "http://localhost:5173/auth/login/oauth2/"
19+
OAUTH_DEXIDP_OIDC_CONFIGURATION_ENDPOINT: "http://127.0.0.1:5556/dex/.well-known/openid-configuration"
20+
PUBLIC_OAUTH_CLIENT_NAME: dexidp
21+
1522
strategy:
1623
matrix:
1724
node-version: ['16', '18', '20']
@@ -30,6 +37,11 @@ jobs:
3037
ports:
3138
- 5432:5432
3239

40+
oauth2:
41+
image: ghcr.io/fractal-analytics-platform/oauth:0.1
42+
ports:
43+
- 5556:5556
44+
3345
steps:
3446
- name: Check out repo
3547
uses: actions/checkout@v4

CHANGELOG.md

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

3+
# Unreleased
4+
5+
* Used links instead of the "Open" button (\#542);
6+
* Added documentation about systemd service config (\#542);
7+
* Added e2e testing of OAuth2 login (\#542);
8+
* Improved handling of validation errors on forms and documentation about error handling (\#542);
9+
* Added `white-space: pre-wrap` on all `pre` tags (\#542);
10+
311
# 1.4.0
412

513
* Fixed input filters lost after version update (\#535);

__tests__/AlertError.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { AlertError } from '../src/lib/common/errors';
3+
4+
describe('AlertError class', () => {
5+
it('Extract string detail for generic error', () => {
6+
const error = new AlertError({ detail: 'error message' }, 422);
7+
expect(error.getSimpleValidationMessage()).eq('error message');
8+
});
9+
10+
it('Extract array detail for generic error', () => {
11+
const error = new AlertError({ detail: ['error message'] }, 422);
12+
expect(error.getSimpleValidationMessage()).eq('error message');
13+
});
14+
15+
it('Extract __root__ generic error', () => {
16+
const error = new AlertError(
17+
{
18+
detail: [
19+
{
20+
loc: ['body', '__root__'],
21+
msg: 'error message',
22+
type: 'value_error'
23+
}
24+
]
25+
},
26+
422
27+
);
28+
expect(error.getSimpleValidationMessage()).eq('error message');
29+
});
30+
31+
it('Extract field error', () => {
32+
const error = new AlertError(
33+
{
34+
detail: [
35+
{
36+
loc: ['body', 'zarr_url'],
37+
msg: 'error message',
38+
type: 'value_error'
39+
}
40+
]
41+
},
42+
422
43+
);
44+
expect(error.getSimpleValidationMessage('zarr_url')).eq('error message');
45+
});
46+
});

docs/development/error-handling.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Error handling
2+
3+
This page describes which coding patterns are used by fractal-web to handle various error cases.
4+
5+
## Fractal-server errors structure
6+
7+
Fractal-server error responses payloads are usually JSON structures having the error under a `detail` key. This is not true for the 500 Internal Server Error, which doesn't provide a JSON payload.
8+
9+
Generic fractal-server errors contain the error message string directly as `detail` value or as an array of values:
10+
11+
```json
12+
{ "detail": "this is the error message" }
13+
```
14+
15+
```json
16+
{ "detail": ["this is also an error"] }
17+
```
18+
19+
Validation errors associated with a specific request field are represented using a `loc` array that describes the position of the invalid field in the request payload:
20+
21+
```json
22+
{
23+
"detail": [
24+
{
25+
"loc": [
26+
"body",
27+
"zarr_url"
28+
],
29+
"msg": "URLs must begin with '/' or 's3'.",
30+
"type": "value_error"
31+
}
32+
]
33+
}
34+
```
35+
36+
Some validation errors may not be associated with a specific field and in that case their `loc` array will reference a `__root__` element:
37+
38+
```json
39+
{
40+
"detail": [
41+
{
42+
"loc": [
43+
"body",
44+
"__root__"
45+
],
46+
"msg": "error message",
47+
"type": "value_error"
48+
}
49+
]
50+
}
51+
```
52+
53+
The goal of fractal-web is to extract the error message and display it inside an alert component or directly near the invalid form field, when possible. If an unexpected JSON structure is received, fractal-web will display the error JSON payload as it is, but that should happen rarely, except for the JSON Schema form, whose errors may result in some complex payloads.
54+
55+
## Error responses in Svelte backend (SSR)
56+
57+
Files in `src/lib/server/api` provide API calls to fractal-server to be used from Svelte backend. These calls are usually required to be successful in order to properly display the page, since they retrieve the main resources of the page. A failure at this level is usually a 404 error (e.g. attempting to open a project with a non existent id) or something really severe (500 errors). For this reason the API calls errors happening on Svelte backend should usually be directly propagated, in order to display the error code inside the page.
58+
59+
The utility function `responseError()` (in `error.js`) can be used to propagate the error in these cases. It will throw an exception if an error response is detected, automatically extracting the `detail`.
60+
61+
It is suggested to handle the unsuccessful response first, in order to return the payload at the end of the function.
62+
63+
```javascript
64+
if (!response.ok) {
65+
await responseError(response);
66+
}
67+
return await response.json();
68+
```
69+
70+
In this way it is easier to define properly the type of the response using JSDoc annotation.
71+
72+
## Error responses in Svelte frontend
73+
74+
### The AlertError class
75+
76+
The `AlertError` class represents errors handled by fractal-web that has to be displayed somewhere. It has a constructor that receives an object or string representing the error and an optional status code (if the error was originated from an unsuccessful API call).
77+
78+
It can be used to istantiate a new error message; this is mostly used when we need to propagate an error from a component to another, that will catch the error:
79+
80+
```javascript
81+
throw new AlertError('Invalid JSON schema');
82+
```
83+
84+
Most of the time it is used to handle an unsuccessful API response; the status code is used to check if it is a validation error (status is equals to 422) and it automatically extracts the message from the detail:
85+
86+
```javascript
87+
const result = await response.json();
88+
throw new AlertError(result, response.status);
89+
```
90+
91+
### The standard error alert
92+
93+
It is possible to use the `displayStandardErrorAlert()` function to display a generic error inside an Bootstrap alert component. The function returns a reference to a `StandardErrorAlert` component, that can be used to hide the error invoking its `hide()` function.
94+
95+
Inside the page we have to add a div for the alert component, with a defined id:
96+
97+
```html
98+
<div id="errorAlert-projectInfoModal" />
99+
```
100+
101+
Then we have to define a variable for the alert component:
102+
103+
```javascript
104+
/** @type {import('$lib/components/common/StandardErrorAlert.svelte').default|undefined} */
105+
let errorAlert = undefined;
106+
```
107+
108+
Finally, we invoke the function to display the error:
109+
110+
```javascript
111+
errorAlert = displayStandardErrorAlert(
112+
new AlertError(result, response.status),
113+
'errorAlert-projectInfoModal'
114+
);
115+
```
116+
117+
If we need to hide the error (for example before clicking a submit button again), we can invoke the `hide()` function.
118+
119+
```javascript
120+
errorAlert?.hide();
121+
```
122+
123+
Notice that we are using the optional chaining operator (`?.`), since the variable might be undefined if no error happened previously.
124+
125+
### Form validation errors
126+
127+
A form usually needs an error alert component to display generic errors and a mechanism to display errors associated with specific form fields. This logic has been incapsulated in the `FormErrorHandler` class.
128+
129+
The constructor accepts as first argument the id of the div that will contain the generic error message and as second argument an array containing all the fields that correspond with some input fields in the form.
130+
131+
```javascript
132+
const formErrorHandler = new FormErrorHandler('taskCollectionError', [
133+
'package',
134+
'package_version',
135+
'package_extras',
136+
'python_version'
137+
]);
138+
```
139+
140+
Once created, it is possible to retrieve the `validationErrors` object from the form error handler:
141+
142+
```javascript
143+
const validationErrors = formErrorHandler.getValidationErrorStore();
144+
```
145+
146+
This is a Svelte store referencing a map of errors, so it has to be accessed prepending the `$` symbol: `$validationErrors`, as shown in the example above.
147+
148+
The Bootstrap validation classes are used inside the form: `has-validation` on parent, `is-invalid` on the invalid field and `invalid-feedback` for the message.
149+
150+
```svelte
151+
<div class="input-group has-validation">
152+
<div class="input-group-text">
153+
<label class="font-monospace" for="package">Package</label>
154+
</div>
155+
<input
156+
name="package"
157+
id="package"
158+
type="text"
159+
class="form-control"
160+
required
161+
class:is-invalid={$validationErrors['package']}
162+
bind:value={python_package}
163+
/>
164+
<span class="invalid-feedback">{$validationErrors['package']}</span>
165+
</div>
166+
```
167+
168+
The `handleErrorResponse()` function is used to populate the fields from an error response:
169+
170+
```javascript
171+
if (!response.ok) {
172+
await formErrorHandler.handleErrorResponse(response);
173+
}
174+
```
175+
176+
The class also provides a `clearErrors()` function and some functions to manually add or remove errors (`addValidationError()`, `removeValidationError()`, `setGenericError()`); these are useful to handle some validation directly on the frontend (e.g. required fields), without performing any API calls.

docs/development/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Here are some useful details for `fractal-web` developments:
44

55
* [Development setup](setup.md)
66
* [Code-base structure](structure.md)
7+
* [Error handling](error-handling.md)
8+
* [JSON Schema form module](jschema.md)
79
* [Testing](tests.md)
810
* [Documentation](docs.md)
911
* [Precommit](precommit.md)

docs/development/structure.md

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -248,38 +248,3 @@ about [server-only modules](https://kit.svelte.dev/docs/server-only-modules)
248248
_Stores_ are modules that export svelte store objects that are used by components to manage the state of the
249249
application.
250250
> Note that stores are currently not well-organized or used due to the youth of the client.
251-
252-
## Error handling
253-
254-
The errors received from fractal-server are displayed in error modals without changing the content of their messages. The `displayStandardErrorAlert()` function can be used to easily display an error message alert in a div having a specific id. This function returns a `StandardErrorAlert` object that can be stored in a variable and then used to hide previous error messages calling its `hide()` method.
255-
256-
Here an example:
257-
258-
```javascript
259-
async function myFunction() {
260-
// remove previous error
261-
if (errorAlert) {
262-
errorAlert.hide();
263-
}
264-
265-
const response = await fetch(`/api/v1/something`);
266-
if (response.ok) {
267-
// do something with the result
268-
} else {
269-
const error = await response.json();
270-
// add error alert inside the element having 'errorElement' as id
271-
errorAlert = displayStandardErrorAlert(error, 'errorElement');
272-
}
273-
}
274-
```
275-
276-
When the displaying of the error alert should be handled by the caller function it is possible to throw an `AlertError` that has to be caught by the caller in order to display the message.
277-
278-
Errors happening during SSR should be considered fatal and propagated using the `responseError()` utility function:
279-
280-
```javascript
281-
if (response.ok) {
282-
return await response.json();
283-
}
284-
await responseError(response);
285-
```

docs/development/tests.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ To print Svelte webserver log set the environment variable `DEBUG=pw:webserver`.
1616

1717
To execute the tests seeing the browser add the `--headed` flag or the `--debug` flag if you need to watch them step by step.
1818

19+
By default v2 tests are run. These tests require running a fractal-server instance using `FRACTAL_RUNNER_BACKEND=local_experimental`.
20+
21+
To run v1 tests start playwright setting the environment variable `TEST_VERSION` to `v1`. These tests require running a fractal-server instance using `FRACTAL_RUNNER_BACKEND=local`.
22+
23+
OAuth2 test requires a running instance of dexidp test image and a fractal-server instance configured to use it. To skip OAuth2 test set the environment variable `SKIP_OAUTH_TEST` to `true`.
24+
1925
## Coverage
2026

2127
> Warning: code coverage results are not reliable at the moment

docs/environment-variables.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ The following environment variables can be used to configure fractal-web.
1616
* `LOG_FILE`: the path of the file where logs will be written; by default is unset and no file will be created;
1717
* `LOG_LEVEL_FILE`: the log level of logs that will be written to the file; the default value is `info`;
1818
* `LOG_LEVEL_CONSOLE`: the log level of logs that will be written to the console; the default value is `warn`;
19-
* `FRACTAL_API_V1_MODE`: include/exclude V1 pages and version switcher; the default value is `include`.
19+
* `FRACTAL_API_V1_MODE`: include/exclude V1 pages and version switcher; the default value is `include`;
20+
* `PUBLIC_FRACTAL_VIZARR_VIEWER_URL`: URL to [fractal-vizarr-viewer](https://github.com/fractal-analytics-platform/fractal-vizarr-viewer) service (e.g. http://localhost:3000/vizarr for testing).
2021

2122
When running directly using `node` command these extra variables can also be configured:
2223

docs/quickstart.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,41 @@ node build/
7171
```bash
7272
node --env-file=.env build
7373
```
74+
75+
## Systemd service
76+
77+
To run fractal-web as a Systemd service create the file `/etc/systemd/system/fractal-web.service` with the following content:
78+
79+
```
80+
[Unit]
81+
Description=Fractal Web
82+
After=syslog.target
83+
84+
[Service]
85+
User=fractal
86+
EnvironmentFile=/path/to/fractal-web/.env
87+
ExecStart=/path/to/node /path/to/fractal-web/build
88+
Restart=on-failure
89+
RestartSec=5s
90+
91+
[Install]
92+
WantedBy=multi-user.target
93+
```
94+
95+
Enable the service:
96+
97+
```sh
98+
sudo systemctl enable fractal-web
99+
```
100+
101+
Start the service:
102+
103+
```sh
104+
sudo systemctl start fractal-web
105+
```
106+
107+
Check the service logs:
108+
109+
```sh
110+
sudo journalctl -u fractal-web
111+
```

0 commit comments

Comments
 (0)