Skip to content

Commit 5cbb5cb

Browse files
authored
Merge pull request #551 from fractal-analytics-platform/groups
Supported user groups
2 parents bbe5337 + ba74a74 commit 5cbb5cb

36 files changed

+1036
-72
lines changed

CHANGELOG.md

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

33
# Unreleased
44

5+
* Implemented functionality for creating and displaying user groups (\#551);
56
* Displayed an error when job submission failed before starting execution of tasks (\#548);
67

78
# 1.4.3

__tests__/errors.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getFieldValidationError } from '../src/lib/common/errors';
3+
4+
describe('Error utility functions', () => {
5+
it('get field validation error without specifying loc', () => {
6+
const error = getFieldValidationError(
7+
{
8+
detail: [
9+
{
10+
loc: ['body', 'new_group_ids'],
11+
msg: 'value is not a valid list',
12+
type: 'type_error.list'
13+
}
14+
]
15+
},
16+
422
17+
);
18+
expect(error).eq('value is not a valid list');
19+
});
20+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, it, beforeEach, expect, vi } from 'vitest';
2+
import { fireEvent, render } from '@testing-library/svelte';
3+
import { readable } from 'svelte/store';
4+
5+
// Mocking fetch
6+
global.fetch = vi.fn();
7+
8+
// Mocking the page store
9+
vi.mock('$app/stores', () => {
10+
return {
11+
page: readable({
12+
data: {
13+
users: [
14+
{ id: 1, email: '[email protected]' },
15+
{ id: 2, email: '[email protected]' }
16+
],
17+
group: {
18+
name: 'test',
19+
user_ids: [1]
20+
}
21+
}
22+
})
23+
};
24+
});
25+
26+
// The component to be tested must be imported after the mock setup
27+
import page from '../../src/routes/v2/admin/groups/[groupId]/edit/+page.svelte';
28+
29+
describe('Admin group edit page', () => {
30+
beforeEach(() => {
31+
fetch.mockClear();
32+
});
33+
34+
it('User is added successfully', async () => {
35+
const result = render(page);
36+
37+
fetch.mockResolvedValue({
38+
ok: true,
39+
status: 200,
40+
json: async () => ({ name: 'test', user_ids: [1, 2] })
41+
});
42+
fireEvent.dragStart(result.getByRole('button', { name: '[email protected]' }));
43+
fireEvent.drop(result.getByText(/drag the users here/i));
44+
45+
await result.findByText(/No more users available/);
46+
expect(result.queryByRole('button', { name: '[email protected]' })).null;
47+
});
48+
49+
it('Error happens when user is dragged and dropped', async () => {
50+
const result = render(page);
51+
52+
fetch.mockResolvedValue({
53+
ok: false,
54+
status: 422,
55+
json: async () => ({ detail: 'An error happened' })
56+
});
57+
58+
fireEvent.dragStart(result.getByRole('button', { name: '[email protected]' }));
59+
fireEvent.drop(result.getByText(/drag the users here/i));
60+
61+
await result.findByText(/An error happened/);
62+
expect(result.queryByRole('button', { name: '[email protected]' })).not.null;
63+
});
64+
});

docs/development/structure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Other than the AJAX calls, there are also some calls to fractal-server API done
9797

9898
The login is still using the Svelte action approach, in which we have to extract the data from a formData object and then use it to build a JSON payload to be forwarded to fractal-server.
9999

100-
Consider the code at `src/lib/server/api/v1/auth_api.js:5`:
100+
Consider the code at `src/lib/server/api/auth_api.js:5`:
101101

102102
```javascript
103103
/**

playwright.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default defineConfig({
107107

108108
webServer: [
109109
{
110-
command: './tests/start-test-server.sh 2.3.7',
110+
command: './tests/start-test-server.sh 2.4.0',
111111
port: 8000,
112112
waitForPort: true,
113113
stdout: 'pipe',

src/hooks.server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { env } from '$env/dynamic/private';
2-
import { getCurrentUser } from '$lib/server/api/v1/auth_api';
2+
import { getCurrentUser } from '$lib/server/api/auth_api';
33
import { getLogger } from '$lib/server/logger.js';
44
import { error, redirect } from '@sveltejs/kit';
55

src/lib/common/errors.js

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,7 @@ export class AlertError extends Error {
3535
* @returns {string | null} the validation message, if found
3636
*/
3737
getSimpleValidationMessage(...loc) {
38-
if (!this.simpleValidationMessage) {
39-
return null;
40-
}
41-
if (typeof this.simpleValidationMessage === 'string') {
42-
return this.simpleValidationMessage;
43-
}
44-
if (this.simpleValidationMessage.loc.length !== loc.length) {
45-
return null;
46-
}
47-
for (let i = 0; i < loc.length; i++) {
48-
if (this.simpleValidationMessage.loc[i] !== loc[i]) {
49-
return null;
50-
}
51-
}
52-
return this.simpleValidationMessage.msg;
38+
return extractFieldValidationError(this.simpleValidationMessage, loc);
5339
}
5440
}
5541

@@ -74,7 +60,7 @@ function getSimpleValidationMessage(reason, statusCode) {
7460
if (typeof err === 'string') {
7561
return err;
7662
}
77-
if (!isValueError(err)) {
63+
if (!hasValidationErrorPayload(err)) {
7864
return null;
7965
}
8066
const loc = err.loc.length > 1 && err.loc[0] === 'body' ? err.loc.slice(1) : err.loc;
@@ -84,6 +70,47 @@ function getSimpleValidationMessage(reason, statusCode) {
8470
};
8571
}
8672

73+
/**
74+
* Extract the validation error, assuming that the call can fail only for on one
75+
* field. Returns null if there is no error or if there are multiple errors.
76+
* @param {any} reason
77+
* @param {number | null} statusCode
78+
* @param {string[] | undefined} loc expected location of the validation message,
79+
* undefined if any location is considered valid
80+
* @returns {string | null}
81+
*/
82+
export function getFieldValidationError(reason, statusCode, loc = undefined) {
83+
const simpleValidationMessage = getSimpleValidationMessage(reason, statusCode);
84+
return extractFieldValidationError(simpleValidationMessage, loc);
85+
}
86+
87+
/**
88+
* @param {{ loc: string[], msg: string } | string|null} simpleValidationMessage
89+
* @param {string[] | undefined} loc expected location of the validation message,
90+
* undefined if any location is considered valid
91+
* @returns {string | null} the validation message, if found
92+
*/
93+
function extractFieldValidationError(simpleValidationMessage, loc) {
94+
if (!simpleValidationMessage) {
95+
return null;
96+
}
97+
if (typeof simpleValidationMessage === 'string') {
98+
return simpleValidationMessage;
99+
}
100+
if (loc === undefined) {
101+
return simpleValidationMessage.msg;
102+
}
103+
if (simpleValidationMessage.loc.length !== loc.length) {
104+
return null;
105+
}
106+
for (let i = 0; i < loc.length; i++) {
107+
if (simpleValidationMessage.loc[i] !== loc[i]) {
108+
return null;
109+
}
110+
}
111+
return simpleValidationMessage.msg;
112+
}
113+
87114
/**
88115
* @param {any} reason
89116
* @param {number | null} statusCode
@@ -99,7 +126,7 @@ export function getValidationMessagesMap(reason, statusCode) {
99126
/** @type {{[key: string]: string}} */
100127
const map = {};
101128
for (const error of reason.detail) {
102-
if (!isValueError(error)) {
129+
if (!hasValidationErrorPayload(error)) {
103130
return null;
104131
}
105132
if (error.loc.length !== 2 || error.loc[0] !== 'body') {
@@ -127,8 +154,8 @@ function isValidationError(reason, statusCode) {
127154
* @param {any} err
128155
* @returns {boolean}
129156
*/
130-
function isValueError(err) {
131-
return Array.isArray(err.loc) && !!err.msg && err.type.startsWith('value_error');
157+
function hasValidationErrorPayload(err) {
158+
return Array.isArray(err.loc) && typeof err.msg === 'string' && typeof err.type === 'string';
132159
}
133160

134161
/**

src/lib/common/user_utilities.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Sort users by email.
3+
* @param {import('$lib/types').User} u1
4+
* @param {import('$lib/types').User} u2
5+
*/
6+
export const sortUserByEmailComparator = function (u1, u2) {
7+
return u1.email.localeCompare(u2.email, undefined, { sensitivity: 'base' });
8+
};
9+
10+
/**
11+
* Sort groups by name.
12+
* @param {import('$lib/types').Group} g1
13+
* @param {import('$lib/types').Group} g2
14+
*/
15+
export const sortGroupByNameComparator = function (g1, g2) {
16+
return g1.name.localeCompare(g2.name, undefined, { sensitivity: 'base' });
17+
};

src/lib/components/common/DragAndDropUploader.svelte

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@
9090
</script>
9191

9292
<div
93-
class="dropZone bg-light"
93+
class="droparea bg-light"
9494
on:drop={handleDrop}
9595
on:dragover={handleDragOver}
9696
on:dragleave={handleDragLeave}
97-
class:dragOver
97+
class:active={dragOver}
9898
>
9999
<div class="m-1">
100100
<div class="input-group has-validation">
@@ -123,16 +123,3 @@
123123
<i class="bi bi-file-earmark-arrow-up" /> or drag file here
124124
</p>
125125
</div>
126-
127-
<style>
128-
.dropZone {
129-
outline: 2px dashed #00b3bb;
130-
outline-offset: -8px;
131-
padding: 10px;
132-
border-radius: 3px;
133-
}
134-
135-
.dragOver {
136-
background-color: #c8e5ff !important;
137-
}
138-
</style>

0 commit comments

Comments
 (0)