refactor: api key modals (#23420)

This commit is contained in:
Daniel Dietzler 2025-10-31 13:58:52 +01:00 committed by GitHub
parent 4abaad548a
commit 3531856d1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 216 additions and 175 deletions

View File

@ -0,0 +1,78 @@
<script lang="ts">
import ApiKeyGrid from '$lib/components/user-settings-page/user-api-key-grid.svelte';
import { Permission } from '@immich/sdk';
import { Checkbox, IconButton, Input, Label } from '@immich/ui';
import { mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
selectedPermissions: Permission[];
};
let { selectedPermissions = $bindable([]) }: Props = $props();
const permissions: Record<string, Permission[]> = {};
for (const permission of Object.values(Permission)) {
if (permission === Permission.All) {
continue;
}
const [group] = permission.split('.');
if (!permissions[group]) {
permissions[group] = [];
}
permissions[group].push(permission);
}
let searchValue = $state('');
let allItemsSelected = $derived(selectedPermissions.length === Object.keys(Permission).length - 1);
const matchFilter = (search: string) => {
search = search.toLowerCase();
return ([title, items]: [string, Permission[]]) =>
title.toLowerCase().includes(search) || items.some((item) => item.toLowerCase().includes(search));
};
const onCheckedAllChange = (checked: boolean) => {
selectedPermissions = checked
? Object.values(Permission).filter((permission) => permission !== Permission.All)
: [];
};
const filteredResults = $derived(Object.entries(permissions).filter(matchFilter(searchValue)));
const handleSelectItems = (items: Permission[]) =>
(selectedPermissions = Array.from(new Set([...selectedPermissions, ...items])));
const handleDeselectItems = (items: Permission[]) =>
(selectedPermissions = selectedPermissions.filter((item) => !items.includes(item)));
</script>
<Label label={$t('permission')} for="permission-container" />
<div class="flex items-center gap-2 m-4" id="permission-container">
<Checkbox id="input-select-all" size="tiny" checked={allItemsSelected} onCheckedChange={onCheckedAllChange} />
<Label label={$t('select_all')} for="input-select-all" />
</div>
<div class="ms-4 flex flex-col gap-2">
<Input bind:value={searchValue} placeholder={$t('search')}>
{#snippet trailingIcon()}
{#if searchValue}
<IconButton
icon={mdiClose}
size="small"
variant="ghost"
shape="round"
color="secondary"
class="me-1"
onclick={() => (searchValue = '')}
aria-label={$t('clear')}
/>
{/if}
{/snippet}
</Input>
{#each filteredResults as [title, subItems] (title)}
<ApiKeyGrid {title} {subItems} selectedItems={selectedPermissions} {handleSelectItems} {handleDeselectItems} />
{/each}
</div>

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { dateFormats } from '$lib/constants';
import ApiKeyModal from '$lib/modals/ApiKeyModal.svelte';
import ApiKeyCreateModal from '$lib/modals/ApiKeyCreateModal.svelte';
import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte';
import ApiKeyUpdateModal from '$lib/modals/ApiKeyUpdateModal.svelte';
import { locale } from '$lib/stores/preferences.store';
import { handleError } from '$lib/utils/handle-error';
import { createApiKey, deleteApiKey, getApiKeys, updateApiKey, type ApiKeyResponseDto } from '@immich/sdk';
import { deleteApiKey, getApiKeys, type ApiKeyResponseDto } from '@immich/sdk';
import { Button, IconButton, modalManager, toastManager } from '@immich/ui';
import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -21,49 +22,22 @@
}
const handleCreate = async () => {
const result = await modalManager.show(ApiKeyModal, {
title: $t('new_api_key'),
apiKey: { name: 'API Key', permissions: [] },
submitText: $t('create'),
});
const secret = await modalManager.show(ApiKeyCreateModal);
if (!result) {
if (!secret) {
return;
}
try {
const { secret } = await createApiKey({
apiKeyCreateDto: {
name: result.name,
permissions: result.permissions,
},
});
await modalManager.show(ApiKeySecretModal, { secret });
} catch (error) {
handleError(error, $t('errors.unable_to_create_api_key'));
} finally {
await refreshKeys();
}
await modalManager.show(ApiKeySecretModal, { secret });
await refreshKeys();
};
const handleUpdate = async (key: ApiKeyResponseDto) => {
const result = await modalManager.show(ApiKeyModal, {
title: $t('api_key'),
submitText: $t('save'),
const success = await modalManager.show(ApiKeyUpdateModal, {
apiKey: key,
});
if (!result) {
return;
}
try {
await updateApiKey({ id: key.id, apiKeyUpdateDto: { name: result.name, permissions: result.permissions } });
toastManager.success($t('saved_api_key'));
} catch (error) {
handleError(error, $t('errors.unable_to_save_api_key'));
} finally {
if (success) {
await refreshKeys();
}
};

View File

@ -0,0 +1,60 @@
<script lang="ts">
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
import { handleError } from '$lib/utils/handle-error';
import { createApiKey, Permission } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
import { mdiKeyVariant } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = { onClose: (secret?: string) => void };
const { onClose }: Props = $props();
let name = $state('API Key');
let selectedPermissions = $state<Permission[]>([]);
const isAllPermissions = $derived(selectedPermissions.length === Object.keys(Permission).length - 1);
const onsubmit = async () => {
if (!name) {
toastManager.warning($t('api_key_empty'));
return;
}
if (selectedPermissions.length === 0) {
toastManager.warning($t('permission_empty'));
return;
}
try {
const { secret } = await createApiKey({
apiKeyCreateDto: {
name,
permissions: isAllPermissions ? [Permission.All] : selectedPermissions,
},
});
onClose(secret);
} catch (error) {
handleError(error, $t('errors.unable_to_create_api_key'));
}
};
</script>
<Modal title={$t('new_api_key')} icon={mdiKeyVariant} {onClose} size="giant">
<ModalBody>
<form {onsubmit} autocomplete="off" id="api-key-form">
<div class="mb-4 flex flex-col gap-2">
<Field label={$t('name')}>
<Input bind:value={name} />
</Field>
</div>
<ApiKeyPermissionsPicker bind:selectedPermissions />
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="api-key-form">{$t('create')}</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@ -1,140 +0,0 @@
<script lang="ts">
import ApiKeyGrid from '$lib/components/user-settings-page/user-api-key-grid.svelte';
import { Permission } from '@immich/sdk';
import {
Button,
Checkbox,
Field,
HStack,
IconButton,
Input,
Label,
Modal,
ModalBody,
ModalFooter,
toastManager,
} from '@immich/ui';
import { mdiClose, mdiKeyVariant } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
const matches = (value: string) => {
value = value.toLowerCase();
return ([title, items]: [string, Permission[]]) => {
return title.toLowerCase().includes(value) || items.some((item) => item.toLowerCase().includes(value));
};
};
interface Props {
apiKey: { name: string; permissions: Permission[] };
title: string;
cancelText?: string;
submitText?: string;
onClose: (apiKey?: { name: string; permissions: Permission[] }) => void;
}
let { apiKey = $bindable(), title, cancelText = $t('cancel'), submitText = $t('save'), onClose }: Props = $props();
let name = $derived(apiKey.name);
let selectedItems: Permission[] = $state(apiKey.permissions);
let selectAllItems = $derived(selectedItems.length === Object.keys(Permission).length - 1);
const permissions: Record<string, Permission[]> = {};
for (const permission of Object.values(Permission)) {
if (permission === Permission.All) {
continue;
}
const [group] = permission.split('.');
if (!permissions[group]) {
permissions[group] = [];
}
permissions[group].push(permission);
}
let searchValue = $state('');
let filteredResults = $derived(Object.entries(permissions).filter(matches(searchValue)));
const handleSelectItems = (permissions: Permission[]) => {
selectedItems = Array.from(new Set([...selectedItems, ...permissions]));
};
const handleDeselectItems = (permissions: Permission[]) => {
selectedItems = selectedItems.filter((item) => !permissions.includes(item));
};
const handleSelectAllItems = () => {
selectedItems = selectAllItems ? [] : Object.values(Permission).filter((item) => item !== Permission.All);
};
const handleSubmit = () => {
if (!name) {
toastManager.warning($t('api_key_empty'));
} else if (selectedItems.length === 0) {
toastManager.warning($t('permission_empty'));
} else {
if (selectAllItems) {
onClose({ name, permissions: [Permission.All] });
} else {
onClose({ name, permissions: selectedItems });
}
}
};
const onsubmit = (event: Event) => {
event.preventDefault();
handleSubmit();
};
onMount(() => {
if (apiKey.permissions.includes(Permission.All)) {
handleSelectAllItems();
}
});
</script>
<Modal {title} icon={mdiKeyVariant} {onClose} size="giant">
<ModalBody>
<form {onsubmit} autocomplete="off" id="api-key-form">
<div class="mb-4 flex flex-col gap-2">
<Field label={$t('name')}>
<Input bind:value={name} />
</Field>
</div>
<Label label={$t('permission')} for="permission-container" />
<div class="flex items-center gap-2 m-4" id="permission-container">
<Checkbox id="input-select-all" size="tiny" checked={selectAllItems} onCheckedChange={handleSelectAllItems} />
<Label label={$t('select_all')} for="input-select-all" />
</div>
<div class="ms-4 flex flex-col gap-2">
<Input bind:value={searchValue} placeholder={$t('search')}>
{#snippet trailingIcon()}
{#if searchValue}
<IconButton
icon={mdiClose}
size="small"
variant="ghost"
shape="round"
color="secondary"
class="me-1"
onclick={() => (searchValue = '')}
aria-label={$t('clear')}
/>
{/if}
{/snippet}
</Input>
{#each filteredResults as [title, subItems] (title)}
<ApiKeyGrid {title} {subItems} {selectedItems} {handleSelectItems} {handleDeselectItems} />
{/each}
</div>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{cancelText}</Button>
<Button shape="round" type="submit" fullWidth form="api-key-form">{submitText}</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@ -0,0 +1,69 @@
<script lang="ts">
import ApiKeyPermissionsPicker from '$lib/components/ApiKeyPermissionsPicker.svelte';
import { handleError } from '$lib/utils/handle-error';
import { Permission, updateApiKey } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
import { mdiKeyVariant } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
apiKey: { id: string; name: string; permissions: Permission[] };
onClose: (success?: true) => void;
}
let { apiKey, onClose }: Props = $props();
const mapPermissions = (permissions: Permission[]) =>
permissions.includes(Permission.All)
? Object.values(Permission).filter((permission) => permission !== Permission.All)
: permissions;
const isAllPermissions = (permissions: Permission[]) => permissions.length === Object.keys(Permission).length - 1;
let name = $state(apiKey.name);
let selectedPermissions = $state<Permission[]>(mapPermissions(apiKey.permissions));
const onsubmit = async () => {
if (!name) {
toastManager.warning($t('api_key_empty'));
return;
}
if (selectedPermissions.length === 0) {
toastManager.warning($t('permission_empty'));
return;
}
try {
await updateApiKey({
id: apiKey.id,
apiKeyUpdateDto: {
name,
permissions: isAllPermissions(selectedPermissions) ? [Permission.All] : selectedPermissions,
},
});
toastManager.success($t('saved_api_key'));
onClose(true);
} catch (error) {
handleError(error, $t('errors.unable_to_save_api_key'));
}
};
</script>
<Modal title={$t('api_key')} icon={mdiKeyVariant} {onClose} size="giant">
<ModalBody>
<form {onsubmit} autocomplete="off" id="api-key-form">
<div class="mb-4 flex flex-col gap-2">
<Field label={$t('name')}>
<Input bind:value={name} />
</Field>
</div>
<ApiKeyPermissionsPicker bind:selectedPermissions />
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="api-key-form">{$t('save')}</Button>
</HStack>
</ModalFooter>
</Modal>