|
14 | 14 | import UserSettingsEditor from './UserSettingsEditor.svelte'; |
15 | 15 | import UserSettingsImportModal from './UserSettingsImportModal.svelte'; |
16 | 16 | import { deepCopy, normalizePayload, nullifyEmptyStrings } from 'fractal-components'; |
| 17 | + import ProfileEditor from './ProfileEditor.svelte'; |
17 | 18 |
|
18 | 19 | /** |
19 | 20 | * @typedef {Object} Props |
|
72 | 73 | let resources = $state([]); |
73 | 74 | /** @type {number|undefined} */ |
74 | 75 | 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'); |
75 | 81 | /** @type {Array<import('fractal-components/types/api').Profile>} */ |
76 | 82 | 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(); |
77 | 87 |
|
78 | 88 | let saving = $state(false); |
79 | 89 | let userFormSubmitted = $state(false); |
|
87 | 97 | /** @type {Modal|undefined} */ |
88 | 98 | let confirmSuperuserChange = $state(); |
89 | 99 |
|
| 100 | + const showCreateProfile = $derived(editableUser && !editableUser.id); |
| 101 | +
|
90 | 102 | async function handleSave() { |
91 | 103 | saving = true; |
92 | 104 | userUpdatedMessage = ''; |
|
120 | 132 | return; |
121 | 133 | } |
122 | 134 | 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 | +
|
123 | 143 | if (password) { |
124 | 144 | editableUser.password = password; |
125 | 145 | } |
|
270 | 290 | loadUserGroups(); |
271 | 291 | await initProfile(); |
272 | 292 | 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 | + } |
273 | 298 | if (selectedResourceId) { |
274 | 299 | await loadProfiles(false); |
275 | 300 | } |
| 301 | + if (editableUser && autoselectProfile && profiles.length > 0) { |
| 302 | + editableUser.profile_id = profiles[0].id; |
| 303 | + } |
276 | 304 | }); |
277 | 305 |
|
278 | 306 | function loadUserGroups() { |
|
319 | 347 | if (editableUser) { |
320 | 348 | editableUser.profile_id = null; |
321 | 349 | } |
| 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 | + } |
322 | 365 | await loadProfiles(); |
323 | 366 | } |
324 | 367 |
|
| 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 | +
|
325 | 398 | async function loadProfiles(hideOldError = true) { |
326 | 399 | if (selectedResourceId === undefined) { |
327 | 400 | return; |
|
521 | 594 | <strong>Profile</strong> |
522 | 595 | </label> |
523 | 596 | <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 | +
|
524 | 626 | <div class="row"> |
525 | 627 | <div class="col-lg-6"> |
526 | 628 | <select |
|
534 | 636 | {/each} |
535 | 637 | </select> |
536 | 638 | </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> |
550 | 659 | </div> |
551 | 660 | </div> |
552 | | - <div id="errorAlert-profiles" class="mt-2 mb-0"></div> |
553 | 661 | </div> |
554 | 662 | </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"> |
555 | 676 | {#if editableUser.id} |
556 | 677 | <div class="row mb-3 has-validation"> |
557 | 678 | <span class="col-sm-3 col-form-label text-end fw-bold">Groups</span> |
|
0 commit comments