Skip to content

Commit bbfc5ba

Browse files
committed
Implemented automatic profile creation during user creation
1 parent 9e2b510 commit bbfc5ba

File tree

3 files changed

+167
-25
lines changed

3 files changed

+167
-25
lines changed

src/lib/components/v2/admin/ProfileEditor.svelte

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
* @property {Omit<import('fractal-components/types/api').Profile, 'id'>} profile
1010
* @property {import('fractal-components/types/api').Resource} resource
1111
* @property {(user: import('fractal-components/types/api').Profile & { id: number | undefined }) => Promise<Response>} saveProfile
12+
* @property {boolean} [showSaveButton]
1213
*/
1314
1415
/** @type {Props} */
15-
let { profile = $bindable(), resource, saveProfile } = $props();
16+
let { profile = $bindable(), resource, saveProfile, showSaveButton = true } = $props();
1617
1718
/** @type {import('fractal-components/types/api').Profile | undefined} */
1819
let editableProfile = $state();
@@ -32,7 +33,10 @@
3233
3334
const profileValidationErrors = profileFormErrorHandler.getValidationErrorStore();
3435
35-
async function handleSave() {
36+
/**
37+
* @returns {Promise<number | undefined>} the profile id, if successfully created
38+
*/
39+
export async function handleSave() {
3640
if (!editableProfile) {
3741
return;
3842
}
@@ -50,8 +54,11 @@
5054
}
5155
if (editableProfile.id) {
5256
profileUpdatedMessage = 'Profile successfully updated';
53-
} else {
57+
} else if (showSaveButton) {
5458
await goto(`/v2/admin/resources/${profile.resource_id}/profiles`);
59+
} else {
60+
const { id } = await response.json();
61+
return id;
5562
}
5663
} finally {
5764
saving = false;
@@ -80,7 +87,7 @@
8087
{/if}
8188
<div class="row mb-3 has-validation">
8289
<label for="name" class="col-sm-3 col-form-label text-end">
83-
<strong>Name</strong>
90+
<strong>Profile name</strong>
8491
</label>
8592
<div class="col-sm-9">
8693
<input
@@ -179,13 +186,15 @@
179186
<div class="col-sm-9 offset-sm-3">
180187
<StandardDismissableAlert message={profileUpdatedMessage} />
181188
<div id="genericProfileError"></div>
182-
<button type="button" onclick={handleSave} class="btn btn-primary" disabled={saving}>
183-
{#if saving}
184-
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true">
185-
</span>
186-
{/if}
187-
Save
188-
</button>
189+
{#if showSaveButton}
190+
<button type="button" onclick={handleSave} class="btn btn-primary" disabled={saving}>
191+
{#if saving}
192+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true">
193+
</span>
194+
{/if}
195+
Save
196+
</button>
197+
{/if}
189198
</div>
190199
</div>
191200
</div>

src/lib/components/v2/admin/UserEditor.svelte

Lines changed: 135 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import UserSettingsEditor from './UserSettingsEditor.svelte';
1515
import UserSettingsImportModal from './UserSettingsImportModal.svelte';
1616
import { deepCopy, normalizePayload, nullifyEmptyStrings } from 'fractal-components';
17+
import ProfileEditor from './ProfileEditor.svelte';
1718
1819
/**
1920
* @typedef {Object} Props
@@ -72,8 +73,17 @@
7273
let resources = $state([]);
7374
/** @type {number|undefined} */
7475
let selectedResourceId = $state();
76+
/** @type {import('fractal-components/types/api').Resource|undefined} */
77+
let selectedResource = $state();
78+
79+
/** @type {'create_new'|'use_existing'} */
80+
let profileOption = $state('use_existing');
7581
/** @type {Array<import('fractal-components/types/api').Profile>} */
7682
let profiles = $state([]);
83+
/** @type {Omit<import('fractal-components/types/api').Profile, 'id'>|undefined} */
84+
let newProfile = $state();
85+
/** @type {import('$lib/components/v2/admin/ProfileEditor.svelte').default|undefined} */
86+
let profileEditor = $state();
7787
7888
let saving = $state(false);
7989
let userFormSubmitted = $state(false);
@@ -87,6 +97,8 @@
8797
/** @type {Modal|undefined} */
8898
let confirmSuperuserChange = $state();
8999
100+
const showCreateProfile = $derived(editableUser && !editableUser.id);
101+
90102
async function handleSave() {
91103
saving = true;
92104
userUpdatedMessage = '';
@@ -120,6 +132,14 @@
120132
return;
121133
}
122134
if (userPendingChanges || password) {
135+
if (showCreateProfile && profileOption === 'create_new' && profileEditor) {
136+
const newProfileId = await profileEditor.handleSave();
137+
if (!newProfileId) {
138+
return;
139+
}
140+
editableUser.profile_id = newProfileId;
141+
}
142+
123143
if (password) {
124144
editableUser.password = password;
125145
}
@@ -270,9 +290,17 @@
270290
loadUserGroups();
271291
await initProfile();
272292
await loadResources(false);
293+
const autoselectProfile = editableUser && !editableUser.id && runnerBackend === 'local';
294+
if (autoselectProfile && resources.length > 0) {
295+
selectedResourceId = resources[0].id;
296+
selectedResource = resources[0];
297+
}
273298
if (selectedResourceId) {
274299
await loadProfiles(false);
275300
}
301+
if (editableUser && autoselectProfile && profiles.length > 0) {
302+
editableUser.profile_id = profiles[0].id;
303+
}
276304
});
277305
278306
function loadUserGroups() {
@@ -319,9 +347,54 @@
319347
if (editableUser) {
320348
editableUser.profile_id = null;
321349
}
350+
351+
if (showCreateProfile) {
352+
await loadResource();
353+
if (selectedResource) {
354+
newProfile = {
355+
resource_id: selectedResource.id,
356+
resource_type: selectedResource.type,
357+
name: '',
358+
jobs_remote_dir: '',
359+
ssh_key_path: '',
360+
tasks_remote_dir: '',
361+
username: ''
362+
};
363+
}
364+
}
322365
await loadProfiles();
323366
}
324367
368+
async function loadResource() {
369+
if (!selectedResourceId) {
370+
selectedResource = undefined;
371+
return;
372+
}
373+
const response = await fetch(`/api/admin/v2/resource/${selectedResourceId}`);
374+
if (response.ok) {
375+
selectedResource = await response.json();
376+
} else {
377+
profilesErrorAlert = displayStandardErrorAlert(
378+
await getAlertErrorFromResponse(response),
379+
'errorAlert-profiles'
380+
);
381+
}
382+
}
383+
384+
/**
385+
* @param {Omit<import('fractal-components/types/api').Profile, 'id'>} profile
386+
*/
387+
async function createProfile(profile) {
388+
const headers = new Headers();
389+
headers.set('Content-Type', 'application/json');
390+
return await fetch(`/api/admin/v2/resource/${selectedResourceId}/profile`, {
391+
method: 'POST',
392+
credentials: 'include',
393+
headers,
394+
body: normalizePayload({ ...profile, resource_id: undefined }, { nullifyEmptyStrings: true })
395+
});
396+
}
397+
325398
async function loadProfiles(hideOldError = true) {
326399
if (selectedResourceId === undefined) {
327400
return;
@@ -521,6 +594,35 @@
521594
<strong>Profile</strong>
522595
</label>
523596
<div class="col-sm-9">
597+
{#if showCreateProfile}
598+
<div class="row mb-1">
599+
<div class="col">
600+
<div class="form-check form-check-inline">
601+
<input
602+
class="form-check-input"
603+
type="radio"
604+
name="profileOptions"
605+
id="use_existing"
606+
value="use_existing"
607+
bind:group={profileOption}
608+
/>
609+
<label class="form-check-label" for="use_existing">Use existing</label>
610+
</div>
611+
<div class="form-check form-check-inline">
612+
<input
613+
class="form-check-input"
614+
type="radio"
615+
name="profileOptions"
616+
id="create_new"
617+
value="create_new"
618+
bind:group={profileOption}
619+
/>
620+
<label class="form-check-label" for="create_new">Create new</label>
621+
</div>
622+
</div>
623+
</div>
624+
{/if}
625+
524626
<div class="row">
525627
<div class="col-lg-6">
526628
<select
@@ -534,24 +636,43 @@
534636
{/each}
535637
</select>
536638
</div>
537-
<div class="col-lg-6">
538-
<select
539-
class="form-select"
540-
bind:value={editableUser.profile_id}
541-
class:is-invalid={userFormSubmitted && $userValidationErrors['profile_id']}
542-
disabled={selectedResourceId === undefined}
543-
>
544-
<option value={null}>Select profile...</option>
545-
{#each profiles as profile (profile.id)}
546-
<option value={profile.id}>{profile.name}</option>
547-
{/each}
548-
</select>
549-
<span class="invalid-feedback">{$userValidationErrors['profile_id']}</span>
639+
{#if profileOption === 'use_existing'}
640+
<div class="col-lg-6">
641+
<select
642+
class="form-select"
643+
bind:value={editableUser.profile_id}
644+
class:is-invalid={userFormSubmitted && $userValidationErrors['profile_id']}
645+
disabled={selectedResourceId === undefined}
646+
>
647+
<option value={null}>Select profile...</option>
648+
{#each profiles as profile (profile.id)}
649+
<option value={profile.id}>{profile.name}</option>
650+
{/each}
651+
</select>
652+
<span class="invalid-feedback">{$userValidationErrors['profile_id']}</span>
653+
</div>
654+
{/if}
655+
</div>
656+
<div class="row">
657+
<div class="col">
658+
<div id="errorAlert-profiles" class="mt-2 mb-0"></div>
550659
</div>
551660
</div>
552-
<div id="errorAlert-profiles" class="mt-2 mb-0"></div>
553661
</div>
554662
</div>
663+
</div>
664+
</div>
665+
{#if showCreateProfile && profileOption === 'create_new' && selectedResource && newProfile}
666+
<ProfileEditor
667+
profile={newProfile}
668+
resource={selectedResource}
669+
saveProfile={createProfile}
670+
showSaveButton={false}
671+
bind:this={profileEditor}
672+
/>
673+
{/if}
674+
<div class="row">
675+
<div class="col-lg-7 needs-validation">
555676
{#if editableUser.id}
556677
<div class="row mb-3 has-validation">
557678
<span class="col-sm-3 col-form-label text-end fw-bold">Groups</span>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { env } from '$env/dynamic/private';
2+
import { getLogger } from '$lib/server/logger.js';
3+
4+
const logger = getLogger('admin create user page');
5+
6+
export async function load() {
7+
logger.trace('Loading create user page');
8+
9+
return {
10+
runnerBackend: env.FRACTAL_RUNNER_BACKEND
11+
};
12+
}

0 commit comments

Comments
 (0)