Skip to content

Commit 25fd21e

Browse files
authored
Merge pull request #571 from fractal-analytics-platform/viewer-paths
Supported viewer paths
2 parents 3e94f5d + febe147 commit 25fd21e

File tree

13 files changed

+253
-9
lines changed

13 files changed

+253
-9
lines changed

CHANGELOG.md

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

3+
# Unreleased
4+
5+
* Added viewer paths editor in admin area (\#571);
6+
* Added user viewer paths page (\#571);
7+
38
# 1.7.1
49

510
* Allowed null values in settings API requests (\#570);

__tests__/v2/admin_group_edit_page.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ vi.mock('$app/stores', () => {
1616
],
1717
group: {
1818
name: 'test',
19-
user_ids: [1]
19+
user_ids: [1],
20+
viewer_paths: []
2021
}
2122
}
2223
})

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.6.2',
110+
command: './tests/start-test-server.sh 2.6.3',
111111
port: 8000,
112112
waitForPort: true,
113113
stdout: 'pipe',

src/lib/server/api/auth_api.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,27 @@ export async function getCurrentUserSettings(fetch) {
6969
return await response.json();
7070
}
7171

72+
/**
73+
* Fetches user viewer paths
74+
* @param {typeof fetch} fetch
75+
* @returns {Promise<string[]>}
76+
*/
77+
export async function getCurrentUserViewerPaths(fetch) {
78+
logger.debug('Retrieving current user viewer paths');
79+
const url = `${env.FRACTAL_SERVER_HOST}/auth/current-user/viewer-paths/`;
80+
const response = await fetch(url, {
81+
method: 'GET',
82+
credentials: 'include'
83+
});
84+
85+
if (!response.ok) {
86+
logger.error('Unable to retrieve the current user viewer paths');
87+
await responseError(response);
88+
}
89+
90+
return await response.json();
91+
}
92+
7293
/**
7394
* Requests to close a user session on the server
7495
* @param {typeof fetch} fetch

src/lib/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export type Group = {
139139
name: string
140140
timestamp_created: string
141141
user_ids?: number[]
142+
viewer_paths: string[]
142143
}
143144

144145
export type TaskCollectStatus = 'pending' | 'installing' | 'collecting' | 'fail' | 'OK';

src/routes/+layout.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { afterNavigate, invalidateAll } from '$app/navigation';
44
import { page } from '$app/stores';
55
import { navigating } from '$app/stores';
6+
import { env } from '$env/dynamic/public';
67
import { reloadVersionedPage } from '$lib/common/selected_api_version';
78
import { onMount } from 'svelte';
89
@@ -199,6 +200,9 @@
199200
{#if $page.data.runnerBackend !== 'local' && $page.data.runnerBackend !== 'local_experimental'}
200201
<li><a class="dropdown-item" href="/settings">My settings</a></li>
201202
{/if}
203+
{#if env.PUBLIC_FRACTAL_VIZARR_VIEWER_URL}
204+
<li><a class="dropdown-item" href="/viewer-paths">Viewer paths</a></li>
205+
{/if}
202206
<li><a class="dropdown-item" href="/auth/logout">Logout</a></li>
203207
</ul>
204208
</li>

src/routes/v2/admin/groups/+page.server.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { sortGroupByNameComparator } from '$lib/common/user_utilities';
12
import { listGroups } from '$lib/server/api/auth_api';
23
import { getLogger } from '$lib/server/logger.js';
34

@@ -8,6 +9,8 @@ export async function load({ fetch }) {
89

910
const groups = await listGroups(fetch, true);
1011

12+
groups.sort(sortGroupByNameComparator);
13+
1114
return {
1215
groups
1316
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<th>Id</th>
6969
<th>Name</th>
7070
<th>Number of users</th>
71+
<th>Number of viewer paths</th>
7172
<th>Actions</th>
7273
</tr>
7374
</thead>
@@ -77,6 +78,7 @@
7778
<td>{group.id}</td>
7879
<td>{group.name}</td>
7980
<td>{group.user_ids.length}</td>
81+
<td>{group.viewer_paths.length}</td>
8082
<td>
8183
<a href="/v2/admin/groups/{group.id}" class="btn btn-light">
8284
<i class="bi-info-circle" /> Info

src/routes/v2/admin/groups/[groupId]/+page.svelte

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@
3434
<h4 class="fw-light mt-4 mb-3">Members of the group</h4>
3535

3636
{#if group.user_ids.length === 0}
37-
<p>
38-
This group has no users.
39-
<a href="/v2/admin/groups/{group.id}/edit" class="btn btn-primary ms-3">Edit</a>
40-
</p>
37+
<p>This group has no users.</p>
4138
{:else}
4239
<div class="col-6 bg-light p-2 rounded">
4340
{#each groupUsers as user}
@@ -47,3 +44,18 @@
4744
{/each}
4845
</div>
4946
{/if}
47+
48+
<div class="row mt-4">
49+
<div class="col">
50+
<h4 class="fw-light mb-3">Viewer paths</h4>
51+
{#if group.viewer_paths.length > 0}
52+
<ul>
53+
{#each group.viewer_paths as viewerPath}
54+
<li><code>{viewerPath}</code></li>
55+
{/each}
56+
</ul>
57+
{:else}
58+
This group has no viewer paths.
59+
{/if}
60+
</div>
61+
</div>

src/routes/v2/admin/groups/[groupId]/edit/+page.svelte

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
<script>
22
import { page } from '$app/stores';
3-
import { AlertError, displayStandardErrorAlert } from '$lib/common/errors';
3+
import { AlertError, displayStandardErrorAlert, FormErrorHandler } from '$lib/common/errors';
44
import { sortUserByEmailComparator } from '$lib/common/user_utilities';
5+
import StandardDismissableAlert from '$lib/components/common/StandardDismissableAlert.svelte';
6+
import { onMount } from 'svelte';
57
68
/** @type {import('$lib/types').Group & {user_ids: number[]}} */
7-
$: group = $page.data.group;
9+
let group = $page.data.group;
810
/** @type {Array<import('$lib/types').User & {id: number}>} */
9-
$: users = $page.data.users;
11+
let users = $page.data.users;
1012
1113
/** @type {import('$lib/types').User & {id: number}|null} */
1214
let draggedUser = null;
@@ -69,6 +71,49 @@
6971
addingUser = null;
7072
hovering = false;
7173
}
74+
75+
/** @type {string[]} */
76+
let editableViewPaths = [];
77+
let viewerPathsUpdatedMessage = '';
78+
const viewerPathsErrorHandler = new FormErrorHandler('viewerPathGenericError', ['viewer_paths']);
79+
const viewerPathsValidationErrors = viewerPathsErrorHandler.getValidationErrorStore();
80+
81+
function addViewerPath() {
82+
editableViewPaths = [...editableViewPaths, ''];
83+
}
84+
85+
/**
86+
* @param {number} index
87+
*/
88+
function removeViewerPath(index) {
89+
editableViewPaths = editableViewPaths.filter((_, i) => i !== index);
90+
}
91+
92+
async function saveViewerPaths() {
93+
viewerPathsUpdatedMessage = '';
94+
viewerPathsErrorHandler.clearErrors();
95+
const headers = new Headers();
96+
headers.set('Content-Type', 'application/json');
97+
const response = await fetch(`/api/auth/group/${group.id}`, {
98+
method: 'PATCH',
99+
credentials: 'include',
100+
headers,
101+
body: JSON.stringify({
102+
viewer_paths: [...editableViewPaths]
103+
})
104+
});
105+
if (!response.ok) {
106+
await viewerPathsErrorHandler.handleErrorResponse(response);
107+
return;
108+
}
109+
const { viewer_paths } = await response.json();
110+
editableViewPaths = viewer_paths;
111+
viewerPathsUpdatedMessage = 'Paths successfully updated';
112+
}
113+
114+
onMount(() => {
115+
editableViewPaths = [...group.viewer_paths];
116+
});
72117
</script>
73118
74119
<nav aria-label="breadcrumb">
@@ -146,3 +191,39 @@
146191
{/if}
147192
</div>
148193
</div>
194+
195+
<div class="row mt-4">
196+
<div class="col-lg-9">
197+
<h4 class="fw-light">Viewer paths</h4>
198+
{#each editableViewPaths as viewerPath, i}
199+
<div class="input-group mb-2">
200+
<input
201+
type="text"
202+
class="form-control"
203+
id={`viewerPath-${i}`}
204+
bind:value={viewerPath}
205+
aria-label={`Viewer path #${i + 1}`}
206+
required
207+
/>
208+
<button
209+
class="btn btn-outline-secondary"
210+
type="button"
211+
id="viewer_path_remove_{i}"
212+
aria-label={`Remove viewer path #${i + 1}`}
213+
on:click={() => removeViewerPath(i)}
214+
>
215+
<i class="bi bi-trash" />
216+
</button>
217+
</div>
218+
{/each}
219+
<button class="btn btn-secondary mb-2" on:click={addViewerPath}>Add viewer path</button>
220+
<div id="viewerPathGenericError" />
221+
{#if $viewerPathsValidationErrors['viewer_paths']}
222+
<div class="alert alert-danger mb-2">
223+
{$viewerPathsValidationErrors['viewer_paths']}
224+
</div>
225+
{/if}
226+
<StandardDismissableAlert message={viewerPathsUpdatedMessage} />
227+
<button class="btn btn-primary" on:click={saveViewerPaths}>Save</button>
228+
</div>
229+
</div>

0 commit comments

Comments
 (0)