feat(web): api key permission search (#20248)

This commit is contained in:
Jason Rasmussen 2025-07-25 13:39:48 -04:00 committed by GitHub
parent da80b69062
commit 153bb70f6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 58 additions and 148 deletions

View File

@ -1,9 +1,9 @@
<script lang="ts">
import type { SearchOptions } from '$lib/utils/dipatch';
import { mdiClose, mdiMagnify } from '@mdi/js';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { t } from 'svelte-i18n';
import { IconButton } from '@immich/ui';
import { mdiClose, mdiMagnify } from '@mdi/js';
import { t } from 'svelte-i18n';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
interface Props {
name: string;

View File

@ -31,7 +31,7 @@
};
</script>
<div class="mx-4 my-2 border bg-subtle dark:bg-black/30 dark:border-black p-4 rounded-2xl">
<div class="border bg-subtle dark:bg-black/30 dark:border-black p-4 rounded-2xl">
<div class="flex items-center gap-2">
<Checkbox
id="permission-{title}"

View File

@ -5,11 +5,17 @@
} from '$lib/components/shared-components/notification/notification';
import ApiKeyGrid from '$lib/components/user-settings-page/user-api-key-grid.svelte';
import { Permission } from '@immich/sdk';
import { Button, Checkbox, HStack, Label, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiKeyVariant } from '@mdi/js';
import { Button, Checkbox, Field, HStack, IconButton, Input, Label, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiClose, mdiKeyVariant } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { SvelteMap } from 'svelte/reactivity';
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[] };
@ -20,137 +26,26 @@
}
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: Map<string, Permission[]> = new SvelteMap();
const permissions: Record<string, Permission[]> = {};
for (const permission of Object.values(Permission)) {
if (permission === Permission.All) {
continue;
}
permissions.set('activity', [
Permission.ActivityCreate,
Permission.ActivityRead,
Permission.ActivityUpdate,
Permission.ActivityDelete,
Permission.ActivityStatistics,
]);
const [group] = permission.split('.');
if (!permissions[group]) {
permissions[group] = [];
}
permissions[group].push(permission);
}
permissions.set('api_key', [
Permission.ApiKeyCreate,
Permission.ApiKeyRead,
Permission.ApiKeyUpdate,
Permission.ApiKeyDelete,
]);
permissions.set('asset', [
Permission.AssetRead,
Permission.AssetUpdate,
Permission.AssetDelete,
Permission.AssetShare,
Permission.AssetView,
Permission.AssetDownload,
Permission.AssetUpload,
]);
permissions.set('album', [
Permission.AlbumCreate,
Permission.AlbumRead,
Permission.AlbumUpdate,
Permission.AlbumDelete,
Permission.AlbumStatistics,
Permission.AlbumAddAsset,
Permission.AlbumRemoveAsset,
Permission.AlbumShare,
Permission.AlbumDownload,
]);
permissions.set('auth_device', [Permission.AuthDeviceDelete]);
permissions.set('archive', [Permission.ArchiveRead]);
permissions.set('face', [Permission.FaceCreate, Permission.FaceRead, Permission.FaceUpdate, Permission.FaceDelete]);
permissions.set('library', [
Permission.LibraryCreate,
Permission.LibraryRead,
Permission.LibraryUpdate,
Permission.LibraryDelete,
Permission.LibraryStatistics,
]);
permissions.set('timeline', [Permission.TimelineRead, Permission.TimelineDownload]);
permissions.set('memory', [
Permission.MemoryCreate,
Permission.MemoryRead,
Permission.MemoryUpdate,
Permission.MemoryDelete,
]);
permissions.set('notification', [
Permission.NotificationCreate,
Permission.NotificationRead,
Permission.NotificationUpdate,
Permission.NotificationDelete,
]);
permissions.set('partner', [
Permission.PartnerCreate,
Permission.PartnerRead,
Permission.PartnerUpdate,
Permission.PartnerDelete,
]);
permissions.set('person', [
Permission.PersonCreate,
Permission.PersonRead,
Permission.PersonUpdate,
Permission.PersonDelete,
Permission.PersonStatistics,
Permission.PersonMerge,
Permission.PersonReassign,
]);
permissions.set('session', [
Permission.SessionCreate,
Permission.SessionRead,
Permission.SessionUpdate,
Permission.SessionDelete,
Permission.SessionLock,
]);
permissions.set('sharedLink', [
Permission.SharedLinkCreate,
Permission.SharedLinkRead,
Permission.SharedLinkUpdate,
Permission.SharedLinkDelete,
]);
permissions.set('stack', [
Permission.StackCreate,
Permission.StackRead,
Permission.StackUpdate,
Permission.StackDelete,
]);
permissions.set('systemConfig', [Permission.SystemConfigRead, Permission.SystemConfigUpdate]);
permissions.set('systemMetadata', [Permission.SystemMetadataRead, Permission.SystemMetadataUpdate]);
permissions.set('tag', [
Permission.TagCreate,
Permission.TagRead,
Permission.TagUpdate,
Permission.TagDelete,
Permission.TagAsset,
]);
permissions.set('adminUser', [
Permission.AdminUserCreate,
Permission.AdminUserRead,
Permission.AdminUserUpdate,
Permission.AdminUserDelete,
]);
let searchValue = $state('');
let filteredResults = $derived(Object.entries(permissions).filter(matches(searchValue)));
const handleSelectItems = (permissions: Permission[]) => {
selectedItems = Array.from(new Set([...selectedItems, ...permissions]));
@ -177,9 +72,9 @@
});
} else {
if (selectAllItems) {
onClose({ name: apiKey.name, permissions: [Permission.All] });
onClose({ name, permissions: [Permission.All] });
} else {
onClose({ name: apiKey.name, permissions: selectedItems });
onClose({ name, permissions: selectedItems });
}
}
};
@ -200,22 +95,37 @@
<ModalBody>
<form {onsubmit} autocomplete="off" id="api-key-form">
<div class="mb-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
<Field label={$t('name')}>
<Input bind:value={name} />
</Field>
</div>
<label class="immich-form-label" for="permission">{$t('permission')}</label>
<div class="flex items-center gap-2 m-4" id="permission">
<Checkbox
id="select-all-permissions"
size="tiny"
checked={selectAllItems}
onCheckedChange={handleSelectAllItems}
/>
<Label label={$t('select_all')} for="select-all-permissions" />
<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>
{#each permissions as [title, subItems] (title)}
<ApiKeyGrid {title} {subItems} {selectedItems} {handleSelectItems} {handleDeselectItems} />
{/each}
</form>
</ModalBody>