refactor: user admin queries (#32490)

This commit is contained in:
Jason Rasmussen
2026-10-17 19:32:35 +03:00
committed by Codex
parent 5910639efd
commit 5351fecc08
21 changed files with 211 additions and 300 deletions
@@ -3,29 +3,17 @@
import { Icon, Text } from '@immich/ui';
import type { Snippet } from 'svelte';
type ValueData = {
value: number;
unit?: ByteUnit | undefined;
};
interface Props {
icon?: string;
title: string;
value?: ValueData;
value?: number;
unit?: ByteUnit;
tooltip?: string;
footer?: Snippet;
}
let { icon, title, value, tooltip, footer }: Props = $props();
const zeros = (data?: ValueData) => {
let length = 13;
if (data) {
const valueLength = data.value.toString().length;
length -= valueLength;
}
return '0'.repeat(length);
};
let { icon, title, value, unit, tooltip, footer }: Props = $props();
const zeros = (value?: number) => '0'.repeat(13 - (value?.toString().length ?? 0));
</script>
<div class="flex h-35 w-full flex-col justify-between rounded-3xl bg-subtle p-5 text-primary">
@@ -36,11 +24,11 @@
<Text size="giant" fontWeight="medium" title={tooltip}>{title}</Text>
</div>
{#if value}
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="{value.value} {value.unit ?? ''}">
<span class="text-gray-300 dark:text-gray-600">{zeros(value)}</span><span>{value.value}</span>
{#if value.unit}
<code class="font-mono text-base font-normal">{value.unit}</code>
{#if value !== undefined}
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="{value} {unit ?? ''}">
<span class="text-gray-300 dark:text-gray-600">{zeros(value)}</span><span>{value}</span>
{#if unit}
<code class="font-mono text-base font-normal">{unit}</code>
{/if}
</div>
{:else}
@@ -79,12 +79,6 @@ export type Events = {
UserPinCodeReset: [];
UserAdminCreate: [UserAdminResponseDto];
UserAdminUpdate: [UserAdminResponseDto];
UserAdminRestore: [UserAdminResponseDto];
// soft deleted
UserAdminDelete: [UserAdminResponseDto];
// confirmed permanently deleted from server
UserAdminDeleted: [{ id: string }];
SessionLocked: [];
@@ -1,7 +1,8 @@
<script lang="ts">
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { handleDeleteUserAdmin } from '$lib/services/user-admin.service';
import { executeMutation } from '$lib/queries/client';
import { deleteUserAdminMutation } from '$lib/queries/user-admin.query';
import { type UserAdminResponseDto } from '@immich/sdk';
import { Alert, Checkbox, ConfirmModal, Field, Input, Label, Text } from '@immich/ui';
import { mdiTrashCanOutline } from '@mdi/js';
@@ -24,10 +25,9 @@
return;
}
const success = await handleDeleteUserAdmin(user, { force });
if (success) {
onClose();
}
await executeMutation(deleteUserAdminMutation, [{ id: user.id, userAdminDeleteDto: { force } }], {
onSuccess: onClose,
});
};
</script>
@@ -1,6 +1,7 @@
<script lang="ts">
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { handleRestoreUserAdmin } from '$lib/services/user-admin.service';
import { executeMutation } from '$lib/queries/client';
import { restoreUserAdminMutation } from '$lib/queries/user-admin.query';
import { type UserAdminResponseDto } from '@immich/sdk';
import { ConfirmModal } from '@immich/ui';
import { mdiDeleteRestore } from '@mdi/js';
@@ -18,10 +19,7 @@
return;
}
const success = await handleRestoreUserAdmin(user);
if (success) {
onClose();
}
await executeMutation(restoreUserAdminMutation, [{ id: user.id }], { onSuccess: onClose });
};
</script>
+2 -2
View File
@@ -10,7 +10,7 @@ import {
import { queryOptions } from '@tanstack/svelte-query';
import { asMutation, queryClient } from '$lib/queries/client';
const findLibrary = (libraries: LibraryResponseDto[], id: string) => {
const findOrFail = (libraries: LibraryResponseDto[], id: string) => {
const library = libraries.find((library) => library.id === id);
if (!library) {
throw new Error(`Library ${id} not found`);
@@ -27,7 +27,7 @@ export const getAllLibrariesQuery = () =>
export const getLibraryQuery = (id: string) =>
queryOptions({
...getAllLibrariesQuery(),
select: (libraries) => findLibrary(libraries, id),
select: (libraries) => findOrFail(libraries, id),
});
export const getLibraryStatisticsQuery = (id: string) =>
+82 -10
View File
@@ -1,20 +1,92 @@
import { searchUsersAdmin } from '@immich/sdk';
import {
CalendarHeatmapType,
createUserAdmin,
deleteUserAdmin,
getUserCalendarHeatmapAdmin,
getUserPreferencesAdmin,
getUserSessionsAdmin,
getUserStatisticsAdmin,
restoreUserAdmin,
searchUsersAdmin,
updateUserAdmin,
type UserAdminResponseDto,
} from '@immich/sdk';
import { queryOptions } from '@tanstack/svelte-query';
import { getHeatmapRange } from '$lib';
import { asMutation, queryClient } from '$lib/queries/client';
export const searchUsersAdminQuery = () =>
const findOrFail = (users: UserAdminResponseDto[], id: string) => {
const user = users.find((user) => user.id === id);
if (!user) {
throw new Error(`User ${id} not found`);
}
return user;
};
export const searchUsersAdminQuery = ({ withDeleted = false }: { withDeleted?: boolean } = {}) =>
queryOptions({
queryKey: ['user-admin'],
queryFn: ({ signal }) => searchUsersAdmin({ withDeleted: true }, { signal }),
select: (users) => (withDeleted ? users : users.filter((user) => !user.deletedAt)),
});
export const getUserAdminQuery = (id: string) =>
queryOptions({
...searchUsersAdminQuery(),
select: (users) => {
const user = users.find((user) => user.id === id);
if (!user) {
throw new Error(`User ${id} not found`);
}
return user;
},
...searchUsersAdminQuery({ withDeleted: true }),
select: (users) => findOrFail(users, id),
});
export const getUserPreferencesAdminQuery = (id: string) =>
queryOptions({
queryKey: ['user-admin', id, 'preferences'],
queryFn: ({ signal }) => getUserPreferencesAdmin({ id }, { signal }),
staleTime: 0,
});
export const getUserStatisticsAdminQuery = (id: string) =>
queryOptions({
queryKey: ['user-admin', id, 'statistics'],
queryFn: ({ signal }) => getUserStatisticsAdmin({ id }, { signal }),
staleTime: 0,
});
export const getUserSessionsAdminQuery = (id: string) =>
queryOptions({
queryKey: ['user-admin', id, 'sessions'],
queryFn: ({ signal }) => getUserSessionsAdmin({ id }, { signal }),
staleTime: 0,
});
export const getUserCalendarHeatmapAdminQuery = (id: string) =>
queryOptions({
queryKey: ['user-admin', id, 'heatmap'],
queryFn: ({ signal }) =>
getUserCalendarHeatmapAdmin({ ...getHeatmapRange(), id, $type: CalendarHeatmapType.Upload }, { signal }),
staleTime: 0,
});
export const createUserAdminMutation = () =>
asMutation(createUserAdmin, {
onSuccess: () => queryClient.invalidateQueries({ queryKey: searchUsersAdminQuery().queryKey, exact: true }),
meta: { error: { toast: 'errors.unable_to_create_user' } },
});
export const updateUserAdminMutation = () =>
asMutation(updateUserAdmin, { onSuccess: onUpdateSuccess, meta: { success: { toast: true } } });
export const deleteUserAdminMutation = () =>
asMutation(deleteUserAdmin, {
onSuccess: onUpdateSuccess,
meta: { success: { toast: true }, error: { toast: 'errors.unable_to_delete_user' } },
});
export const restoreUserAdminMutation = () =>
asMutation(restoreUserAdmin, {
onSuccess: onUpdateSuccess,
meta: { success: { toast: true }, error: { toast: 'errors.unable_to_restore_user' } },
});
const onUpdateSuccess = (user: UserAdminResponseDto) =>
queryClient.setQueryData(searchUsersAdminQuery().queryKey, (users) =>
users?.map((item) => (item.id === user.id ? user : item)),
);
+11 -84
View File
@@ -1,15 +1,5 @@
import {
createUserAdmin,
deleteUserAdmin,
restoreUserAdmin,
updateUserAdmin,
UserStatus,
type UserAdminCreateDto,
type UserAdminDeleteDto,
type UserAdminResponseDto,
type UserAdminUpdateDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { UserStatus, type UserAdminResponseDto } from '@immich/sdk';
import { modalManager, type ActionItem } from '@immich/ui';
import {
mdiDeleteRestore,
mdiInformationOutline,
@@ -23,14 +13,14 @@ import { DateTime } from 'luxon';
import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
import { executeMutation } from '$lib/queries/client';
import { updateUserAdminMutation } from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import type { HeaderButtonActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
export const getUserAdminsActions = ($t: MessageFormatter) => {
@@ -97,60 +87,6 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
return { Detail, Update, Delete, Restore, ResetPassword, ResetPinCode };
};
export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => {
const $t = await getFormatter();
try {
const response = await createUserAdmin({ userAdminCreateDto: dto });
eventManager.emit('UserAdminCreate', response);
toastManager.primary();
return response;
} catch (error) {
handleError(error, $t('errors.unable_to_create_user'));
}
};
export const handleUpdateUserAdmin = async (user: UserAdminResponseDto, dto: UserAdminUpdateDto) => {
const $t = await getFormatter();
try {
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: dto });
eventManager.emit('UserAdminUpdate', response);
toastManager.primary();
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_update_user'));
return false;
}
};
export const handleDeleteUserAdmin = async (user: UserAdminResponseDto, dto: UserAdminDeleteDto) => {
const $t = await getFormatter();
try {
const result = await deleteUserAdmin({ id: user.id, userAdminDeleteDto: dto });
eventManager.emit('UserAdminDelete', result);
toastManager.primary();
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_delete_user'));
}
};
export const handleRestoreUserAdmin = async (user: UserAdminResponseDto) => {
const $t = await getFormatter();
try {
const response = await restoreUserAdmin({ id: user.id });
eventManager.emit('UserAdminRestore', response);
toastManager.primary();
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_restore_user'));
return false;
}
};
// TODO move password reset server-side
const generatePassword = (length: number = 16) => {
let generatedPassword = '';
@@ -176,15 +112,12 @@ const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
return;
}
try {
const dto = { password: generatePassword(), shouldChangePassword: true };
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: dto });
eventManager.emit('UserAdminUpdate', response);
toastManager.primary();
await modalManager.show(PasswordResetSuccessModal, { newPassword: dto.password });
} catch (error) {
handleError(error, $t('errors.unable_to_reset_password'));
}
const password = generatePassword();
await executeMutation(
updateUserAdminMutation,
[{ id: user.id, userAdminUpdateDto: { password, shouldChangePassword: true } }],
{ onSuccess: () => void modalManager.show(PasswordResetSuccessModal, { newPassword: password }) },
);
};
const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => {
@@ -195,11 +128,5 @@ const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => {
return;
}
try {
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
eventManager.emit('UserAdminUpdate', response);
toastManager.primary($t('pin_code_reset_successfully'));
} catch (error) {
handleError(error, $t('errors.unable_to_reset_pin_code'));
}
await executeMutation(updateUserAdminMutation, [{ id: user.id, userAdminUpdateDto: { pinCode: null } }]);
};
+6 -1
View File
@@ -16,6 +16,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { searchUsersAdminQuery } from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import { maintenanceStore } from '$lib/stores/maintenance.store';
import { notificationManager } from '$lib/stores/notification-manager.svelte';
@@ -93,7 +94,11 @@ websocket
})
.on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event))
.on('on_session_delete', () => eventManager.emit('SessionDelete'))
.on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id }))
.on('on_user_delete', (id) => {
queryClient.removeQueries({ queryKey: ['user-admin', id] });
void queryClient.invalidateQueries({ queryKey: searchUsersAdminQuery().queryKey, exact: true });
eventManager.emit('UserAdminDeleted', { id });
})
.on('on_asset_delete', (asset) => eventManager.emit('AssetsDelete', [asset]))
.on('on_asset_trash', (assets) => eventManager.emit('AssetsDelete', assets))
.on('on_asset_update', (asset) => eventManager.emit('AssetUpdate', asset))
@@ -11,7 +11,7 @@
let ownerId: string = $state(authManager.user.id);
const query = createPreloadedQuery(searchUsersAdminQuery);
const users = $derived(query.data.filter((user) => !user.deletedAt));
const users = $derived(query.data);
const onClose = async () => {
await goto(Route.libraries());
@@ -29,15 +29,7 @@
const statisticsQuery = createQuery(() => getLibraryStatisticsQuery(library.id));
const stats = $derived(statisticsQuery.data);
const photos = $derived(stats && { value: stats.photos });
const videos = $derived(stats && { value: stats.videos });
const usage = $derived.by(() => {
if (!stats) {
return;
}
const [value, unit] = getBytesWithUnit(stats.usage);
return { value, unit };
});
const [usage, usageUnit] = $derived(stats ? getBytesWithUnit(stats.usage) : [undefined, undefined]);
const { Edit, Delete, AddFolder, AddExclusionPattern, Scan } = $derived(getLibraryActions($t, library));
</script>
@@ -52,9 +44,9 @@
<div class="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
<Heading tag="h1" size="large" class="col-span-full my-4">{library.name}</Heading>
<div class="col-span-full flex flex-col gap-4 lg:flex-row">
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={photos} />
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={videos} />
<ServerStatisticsCard icon={mdiChartPie} title={$t('usage')} value={usage} />
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={stats?.photos} />
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={stats?.videos} />
<ServerStatisticsCard icon={mdiChartPie} title={$t('usage')} value={usage} unit={usageUnit} />
</div>
<AdminCard icon={mdiFolderOutline} title={$t('folders')} headerAction={AddFolder}>
@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import { queryClient } from '$lib/queries/client';
import { getAllLibrariesQuery } from '$lib/queries/library.query';
import { getLibraryQuery } from '$lib/queries/library.query';
import { Route } from '$lib/route';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
@@ -9,10 +9,7 @@ import type { LayoutLoad } from './$types';
export const load = (async ({ params: { id }, url }) => {
await authenticate(url, { admin: true });
const libraries = await queryClient.query(getAllLibrariesQuery());
if (libraries.every((library) => library.id !== id)) {
redirect(307, Route.libraries());
}
await queryClient.query(getLibraryQuery(id)).catch(() => redirect(307, Route.libraries()));
const $t = await getFormatter();
@@ -143,7 +143,7 @@
<ServerStatisticsCard
title={$t(getReportTypeTranslation(reportType))}
tooltip={$t(getReportTypeDescriptionKey(reportType))}
value={{ value: integrityReport[reportType] }}
value={integrityReport[reportType]}
>
{#snippet footer()}
<HStack gap={1} class="justify-between">
@@ -13,6 +13,6 @@
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
<Container size="large" center>
<ServerStatisticsPanel users={data.users} />
<ServerStatisticsPanel />
</Container>
</AdminPageLayout>
+2 -3
View File
@@ -1,7 +1,7 @@
import { searchUsersAdmin } from '@immich/sdk';
import { noop } from 'lodash-es';
import { queryClient } from '$lib/queries/client';
import { getServerStatisticsQuery } from '$lib/queries/server.query';
import { searchUsersAdminQuery } from '$lib/queries/user-admin.query';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
@@ -9,11 +9,10 @@ import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
void queryClient.query(getServerStatisticsQuery()).catch(noop);
const users = await searchUsersAdmin({ withDeleted: false });
await queryClient.query(searchUsersAdminQuery());
const $t = await getFormatter();
return {
users,
meta: {
title: $t('server_stats'),
},
@@ -1,9 +1,10 @@
<script lang="ts">
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getServerStatisticsQuery } from '$lib/queries/server.query';
import { searchUsersAdminQuery } from '$lib/queries/user-admin.query';
import { locale } from '$lib/stores/preferences.store';
import { getBytesWithUnit } from '$lib/utils/byte-units';
import type { UserAdminResponseDto } from '@immich/sdk';
import {
Code,
FormatBytes,
@@ -20,11 +21,8 @@
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
users: UserAdminResponseDto[];
};
const { users }: Props = $props();
const usersQuery = createPreloadedQuery(searchUsersAdminQuery);
const users = $derived(usersQuery.data);
const query = createQuery(() => ({ ...getServerStatisticsQuery(), refetchInterval: 5000 }));
const stats = $derived(query.data);
@@ -34,15 +32,7 @@
return getBytesWithUnit(usage, usage > TiB ? 2 : 0);
};
const photos = $derived(stats && { value: stats.photos });
const videos = $derived(stats && { value: stats.videos });
const storage = $derived.by(() => {
if (!stats) {
return;
}
const [value, unit] = getStorageUsageWithUnit(stats.usage);
return { value, unit };
});
const [storage, storageUnit] = $derived(stats ? getStorageUsageWithUnit(stats.usage) : [undefined, undefined]);
const zeros = (value: number, maxLength = 13) => {
const valueLength = value.toString().length;
@@ -65,9 +55,9 @@
<Text class="mb-2" fontWeight="medium">{$t('total_usage')}</Text>
<div class="hidden justify-between gap-4 lg:flex">
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={photos} />
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={videos} />
<ServerStatisticsCard icon={mdiChartPie} title={$t('storage')} value={storage} />
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={stats?.photos} />
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={stats?.videos} />
<ServerStatisticsCard icon={mdiChartPie} title={$t('storage')} value={storage} unit={storageUnit} />
</div>
<div class="mt-5 flex lg:hidden">
@@ -1,11 +1,12 @@
<script lang="ts">
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { searchUsersAdminQuery } from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import { getUserAdminActions, getUserAdminsActions } from '$lib/services/user-admin.service';
import { locale } from '$lib/stores/preferences.store';
import { getByteUnitString } from '$lib/utils/byte-units';
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
import type { UserAdminResponseDto } from '@immich/sdk';
import {
CommandPaletteDefaultProvider,
Container,
@@ -32,20 +33,8 @@
let { children, data }: Props = $props();
let users: UserAdminResponseDto[] = $state(data.users);
const onUpdate = async (user: UserAdminResponseDto) => {
const index = users.findIndex(({ id }) => id === user.id);
if (index === -1) {
users = await searchUsersAdmin({ withDeleted: true });
} else {
users[index] = user;
}
};
const onUserAdminDeleted = ({ id: userId }: { id: string }) => {
users = users.filter(({ id }) => id !== userId);
};
const query = createPreloadedQuery(() => searchUsersAdminQuery({ withDeleted: true }));
const users = $derived(query.data);
const { Create } = $derived(getUserAdminsActions($t));
@@ -62,14 +51,6 @@
};
</script>
<OnEvents
onUserAdminCreate={onUpdate}
onUserAdminUpdate={onUpdate}
onUserAdminDelete={onUpdate}
onUserAdminRestore={onUpdate}
{onUserAdminDeleted}
/>
<CommandPaletteDefaultProvider name={$t('users')} actions={[Create]} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[Create]}>
+3 -3
View File
@@ -1,15 +1,15 @@
import { searchUsersAdmin } from '@immich/sdk';
import { queryClient } from '$lib/queries/client';
import { searchUsersAdminQuery } from '$lib/queries/user-admin.query';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const users = await searchUsersAdmin({ withDeleted: true });
await queryClient.query(searchUsersAdminQuery());
const $t = await getFormatter();
return {
users,
meta: {
title: $t('admin.user_management'),
},
@@ -2,8 +2,9 @@
import { goto } from '$app/navigation';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route';
import { handleCreateUserAdmin } from '$lib/services/user-admin.service';
import { executeMutation } from '$lib/queries/client';
import { getStorageQuery } from '$lib/queries/server.query';
import { createUserAdminMutation } from '$lib/queries/user-admin.query';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { Field, FormModal, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
import { createQuery } from '@tanstack/svelte-query';
@@ -45,19 +46,11 @@
isCreatingUser = true;
const user = await handleCreateUserAdmin({
email,
password,
shouldChangePassword,
name,
quotaSizeInBytes,
notify,
isAdmin,
});
if (user) {
await goto(Route.viewUser(user), { replaceState: true });
}
await executeMutation(
createUserAdminMutation,
[{ userAdminCreateDto: { email, password, shouldChangePassword, name, quotaSizeInBytes, notify, isAdmin } }],
{ onSuccess: (user) => void goto(Route.viewUser(user), { replaceState: true }) },
);
isCreatingUser = false;
};
+44 -47
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { goto } from '$app/navigation';
import AdminCard from '$lib/components/AdminCard.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte';
@@ -7,12 +7,19 @@
import UserAvatar from '$lib/components/shared-components/UserAvatar.svelte';
import DeviceCard from '$lib/components/user-settings-page/DeviceCard.svelte';
import FeatureSetting from './FeatureSetting.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import {
getUserAdminQuery,
getUserCalendarHeatmapAdminQuery,
getUserPreferencesAdminQuery,
getUserSessionsAdminQuery,
getUserStatisticsAdminQuery,
} from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import { getUserAdminActions } from '$lib/services/user-admin.service';
import { locale } from '$lib/stores/preferences.store';
import { createDateFormatter, findLocale } from '$lib/utils';
import { getBytesWithUnit } from '$lib/utils/byte-units';
import { CalendarHeatmapType, getUserCalendarHeatmapAdmin, type UserAdminResponseDto } from '@immich/sdk';
import {
Alert,
Badge,
@@ -40,21 +47,27 @@
mdiPlayCircle,
mdiTrashCanOutline,
} from '@mdi/js';
import type { Snippet } from 'svelte';
import { createQuery } from '@tanstack/svelte-query';
import { t } from 'svelte-i18n';
import type { LayoutData } from './$types';
import { getHeatmapRange } from '$lib';
import type { LayoutProps } from './$types';
import Skeleton from '$lib/elements/Skeleton.svelte';
import CalendarHeatmap from '$lib/components/CalendarHeatmap.svelte';
type Props = {
children?: Snippet;
data: LayoutData;
const { children, params }: LayoutProps = $props();
const queries = {
user: createPreloadedQuery(() => getUserAdminQuery(params.id)),
preferences: createQuery(() => getUserPreferencesAdminQuery(params.id)),
statistics: createQuery(() => getUserStatisticsAdminQuery(params.id)),
sessions: createQuery(() => getUserSessionsAdminQuery(params.id)),
heatmap: createQuery(() => getUserCalendarHeatmapAdminQuery(params.id)),
};
const { children, data }: Props = $props();
const { user, userPreferences, userStatistics, userSessions } = $derived(data);
const user = $derived(queries.user.data);
const userPreferences = $derived(queries.preferences.data);
const userStatistics = $derived(queries.statistics.data);
const userSessions = $derived(queries.sessions.data ?? []);
const heatmap = $derived(queries.heatmap.data);
const usedBytes = $derived(user.quotaUsageInBytes ?? 0);
const availableBytes = $derived(user.quotaSizeInBytes ?? 0);
const TiB = 1024 ** 4;
@@ -73,15 +86,6 @@
const { ResetPassword, ResetPinCode, Update, Delete, Restore } = $derived(getUserAdminActions($t, user));
const onUpdate = async (update: UserAdminResponseDto) => {
if (update.id !== user.id) {
return;
}
data.user = update;
await invalidateAll();
};
const onUserAdminDeleted = async ({ id }: { id: string }) => {
if (id === user.id) {
await goto(Route.users());
@@ -89,12 +93,7 @@
};
</script>
<OnEvents
onUserAdminUpdate={onUpdate}
onUserAdminDelete={onUpdate}
onUserAdminRestore={onUpdate}
{onUserAdminDeleted}
/>
<OnEvents {onUserAdminDeleted} />
<CommandPaletteDefaultProvider name={$t('user')} actions={[ResetPassword, ResetPinCode, Update, Delete, Restore]} />
@@ -122,13 +121,9 @@
</div>
<div class="col-span-full">
<div class="flex w-full flex-col gap-4 lg:flex-row">
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={{ value: userStatistics.images }} />
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={{ value: userStatistics.videos }} />
<ServerStatisticsCard
icon={mdiChartPie}
title={$t('storage')}
value={{ value: statsUsage, unit: statsUsageUnit }}
/>
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={userStatistics?.images} />
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={userStatistics?.videos} />
<ServerStatisticsCard icon={mdiChartPie} title={$t('storage')} value={statsUsage} unit={statsUsageUnit} />
</div>
</div>
@@ -159,15 +154,17 @@
<AdminCard icon={mdiFeatureSearchOutline} title={$t('features')}>
<Stack gap={3}>
<FeatureSetting title={$t('email_notifications')} state={userPreferences.emailNotifications.enabled} />
<FeatureSetting title={$t('folders')} state={userPreferences.folders.enabled} />
<FeatureSetting title={$t('memories')} state={userPreferences.memories.enabled} />
<FeatureSetting title={$t('people')} state={userPreferences.people.enabled} />
<FeatureSetting title={$t('rating')} state={userPreferences.ratings.enabled} />
<FeatureSetting title={$t('shared_links')} state={userPreferences.sharedLinks.enabled} />
<FeatureSetting title={$t('show_supporter_badge')} state={userPreferences.purchase.showSupportBadge} />
<FeatureSetting title={$t('tags')} state={userPreferences.tags.enabled} />
<FeatureSetting title={$t('gcast_enabled')} state={userPreferences.cast.gCastEnabled} />
{#if userPreferences}
<FeatureSetting title={$t('email_notifications')} state={userPreferences.emailNotifications.enabled} />
<FeatureSetting title={$t('folders')} state={userPreferences.folders.enabled} />
<FeatureSetting title={$t('memories')} state={userPreferences.memories.enabled} />
<FeatureSetting title={$t('people')} state={userPreferences.people.enabled} />
<FeatureSetting title={$t('rating')} state={userPreferences.ratings.enabled} />
<FeatureSetting title={$t('shared_links')} state={userPreferences.sharedLinks.enabled} />
<FeatureSetting title={$t('show_supporter_badge')} state={userPreferences.purchase.showSupportBadge} />
<FeatureSetting title={$t('tags')} state={userPreferences.tags.enabled} />
<FeatureSetting title={$t('gcast_enabled')} state={userPreferences.cast.gCastEnabled} />
{/if}
</Stack>
</AdminCard>
@@ -210,15 +207,15 @@
<Icon icon={mdiCloudUploadOutline} size="1.5rem" />
<CardTitle>{$t('uploads')}</CardTitle>
</div>
{#await getUserCalendarHeatmapAdmin({ ...getHeatmapRange(), id: user.id, $type: CalendarHeatmapType.Upload })}
<Skeleton height={80} class="mt-2 rounded-lg" />
{:then data}
{#if heatmap}
<CalendarHeatmap
{data}
data={heatmap}
itemLabel={(item) => $t('upload_day_count', { values: item })}
totalLabel={(count) => $t('uploads_count', { values: { count } })}
/>
{/await}
{:else}
<Skeleton height={80} class="mt-2 rounded-lg" />
{/if}
</div>
<!-- </AdminCard> -->
</div>
+3 -15
View File
@@ -1,6 +1,7 @@
import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { UUID_REGEX } from '$lib/constants';
import { queryClient } from '$lib/queries/client';
import { getUserAdminQuery } from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
@@ -13,24 +14,11 @@ export const load = (async ({ params, url }) => {
redirect(307, Route.users());
}
const [user] = await searchUsersAdmin({ id: params.id, withDeleted: true }).catch(() => []);
if (!user) {
redirect(307, Route.users());
}
const [userPreferences, userStatistics, userSessions] = await Promise.all([
getUserPreferencesAdmin({ id: user.id }),
getUserStatisticsAdmin({ id: user.id }),
getUserSessionsAdmin({ id: user.id }),
]);
await queryClient.query(getUserAdminQuery(params.id)).catch(() => redirect(307, Route.users()));
const $t = await getFormatter();
return {
user,
userPreferences,
userStatistics,
userSessions,
meta: {
title: $t('admin.user_details'),
},
@@ -2,25 +2,23 @@
import { goto } from '$app/navigation';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { handleUpdateUserAdmin } from '$lib/services/user-admin.service';
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
import { getStorageQuery } from '$lib/queries/server.query';
import { getUserAdminQuery, updateUserAdminMutation } from '$lib/queries/user-admin.query';
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
import { Field, FormModal, Input, Link, NumberInput, Switch, Text } from '@immich/ui';
import { createQuery } from '@tanstack/svelte-query';
import { mdiAccountEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import type { PageProps } from './$types';
type Props = {
data: PageData;
};
let { data }: Props = $props();
const { params }: PageProps = $props();
const query = createQuery(getStorageQuery);
const serverInfo = $derived(query.data);
const user = $derived(data.user);
const userQuery = createPreloadedQuery(() => getUserAdminQuery(params.id));
const user = $derived(userQuery.data);
let { isAdmin, name, email } = $derived(user);
let storageLabel = $derived(user.storageLabel || '');
const previousQuota = $derived(user.quotaSizeInBytes);
@@ -35,24 +33,16 @@
previousQuota !== quotaSizeBytes && !!quotaSizeBytes && serverInfo && quotaSizeBytes > serverInfo.diskSizeRaw,
);
const onClose = async () => {
await goto(Route.viewUser(user));
};
const onClose = () => void goto(Route.viewUser(user));
const onSubmit = async (event: Event) => {
event.preventDefault();
const success = await handleUpdateUserAdmin(user, {
email,
name,
storageLabel,
quotaSizeInBytes: typeof quotaSize === 'number' ? convertToBytes(quotaSize, ByteUnit.GiB) : null,
isAdmin,
});
if (success) {
await onClose();
}
await executeMutation(
updateUserAdminMutation,
[{ id: user.id, userAdminUpdateDto: { email, name, storageLabel, quotaSizeInBytes: quotaSizeBytes, isAdmin } }],
{ onSuccess: onClose },
);
};
</script>