diff --git a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte
index b374a7696..71f16ba43 100644
--- a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte
+++ b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte
@@ -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));
-
{zeros(value)}{value.value}
- {#if value.unit}
-
{value.unit}
+ {#if value !== undefined}
+
+ {zeros(value)}{value}
+ {#if unit}
+ {unit}
{/if}
{:else}
diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts
index d8d07fcb7..8e0ac5adc 100644
--- a/web/src/lib/managers/event-manager.svelte.ts
+++ b/web/src/lib/managers/event-manager.svelte.ts
@@ -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: [];
diff --git a/web/src/lib/modals/UserDeleteConfirmModal.svelte b/web/src/lib/modals/UserDeleteConfirmModal.svelte
index 34646076c..03818b550 100644
--- a/web/src/lib/modals/UserDeleteConfirmModal.svelte
+++ b/web/src/lib/modals/UserDeleteConfirmModal.svelte
@@ -1,7 +1,8 @@
diff --git a/web/src/lib/modals/UserRestoreConfirmModal.svelte b/web/src/lib/modals/UserRestoreConfirmModal.svelte
index 71546921a..377b90a9d 100644
--- a/web/src/lib/modals/UserRestoreConfirmModal.svelte
+++ b/web/src/lib/modals/UserRestoreConfirmModal.svelte
@@ -1,6 +1,7 @@
diff --git a/web/src/lib/queries/library.query.ts b/web/src/lib/queries/library.query.ts
index 4949ce557..317d6b84d 100644
--- a/web/src/lib/queries/library.query.ts
+++ b/web/src/lib/queries/library.query.ts
@@ -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) =>
diff --git a/web/src/lib/queries/user-admin.query.ts b/web/src/lib/queries/user-admin.query.ts
index bcf010e57..9cf80cf0f 100644
--- a/web/src/lib/queries/user-admin.query.ts
+++ b/web/src/lib/queries/user-admin.query.ts
@@ -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)),
+ );
diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts
index 00807c84b..f266a0c72 100644
--- a/web/src/lib/services/user-admin.service.ts
+++ b/web/src/lib/services/user-admin.service.ts
@@ -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 } }]);
};
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts
index f9832f8c3..301713e89 100644
--- a/web/src/lib/stores/websocket.ts
+++ b/web/src/lib/stores/websocket.ts
@@ -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))
diff --git a/web/src/routes/admin/library-management/(list)/new/+page.svelte b/web/src/routes/admin/library-management/(list)/new/+page.svelte
index b8db1a86f..6dde25581 100644
--- a/web/src/routes/admin/library-management/(list)/new/+page.svelte
+++ b/web/src/routes/admin/library-management/(list)/new/+page.svelte
@@ -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());
diff --git a/web/src/routes/admin/library-management/[id]/+layout.svelte b/web/src/routes/admin/library-management/[id]/+layout.svelte
index 690a01ea1..3c1c1228d 100644
--- a/web/src/routes/admin/library-management/[id]/+layout.svelte
+++ b/web/src/routes/admin/library-management/[id]/+layout.svelte
@@ -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));
@@ -52,9 +44,9 @@
{library.name}
-
-
-
+
+
+
diff --git a/web/src/routes/admin/library-management/[id]/+layout.ts b/web/src/routes/admin/library-management/[id]/+layout.ts
index 017cf68ed..779ee8e06 100644
--- a/web/src/routes/admin/library-management/[id]/+layout.ts
+++ b/web/src/routes/admin/library-management/[id]/+layout.ts
@@ -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();
diff --git a/web/src/routes/admin/maintenance/+page.svelte b/web/src/routes/admin/maintenance/+page.svelte
index a8163c439..3859d1df4 100644
--- a/web/src/routes/admin/maintenance/+page.svelte
+++ b/web/src/routes/admin/maintenance/+page.svelte
@@ -143,7 +143,7 @@
{#snippet footer()}
diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte
index aacfeae60..fe2ac576c 100644
--- a/web/src/routes/admin/server-status/+page.svelte
+++ b/web/src/routes/admin/server-status/+page.svelte
@@ -13,6 +13,6 @@
-
+
diff --git a/web/src/routes/admin/server-status/+page.ts b/web/src/routes/admin/server-status/+page.ts
index 44a7861db..7c168bfb3 100644
--- a/web/src/routes/admin/server-status/+page.ts
+++ b/web/src/routes/admin/server-status/+page.ts
@@ -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'),
},
diff --git a/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte b/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte
index 3c93977d2..a0df72fb8 100644
--- a/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte
+++ b/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte
@@ -1,9 +1,10 @@
-
-
diff --git a/web/src/routes/admin/users/(list)/+layout.ts b/web/src/routes/admin/users/(list)/+layout.ts
index a8941ad8a..ba3812bc3 100644
--- a/web/src/routes/admin/users/(list)/+layout.ts
+++ b/web/src/routes/admin/users/(list)/+layout.ts
@@ -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'),
},
diff --git a/web/src/routes/admin/users/(list)/new/+page.svelte b/web/src/routes/admin/users/(list)/new/+page.svelte
index e0439c85c..e48646f32 100644
--- a/web/src/routes/admin/users/(list)/new/+page.svelte
+++ b/web/src/routes/admin/users/(list)/new/+page.svelte
@@ -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;
};
diff --git a/web/src/routes/admin/users/[id]/+layout.svelte b/web/src/routes/admin/users/[id]/+layout.svelte
index 6726883cb..f60662ac0 100644
--- a/web/src/routes/admin/users/[id]/+layout.svelte
+++ b/web/src/routes/admin/users/[id]/+layout.svelte
@@ -1,5 +1,5 @@
-
+
@@ -122,13 +121,9 @@
@@ -159,15 +154,17 @@
-
-
-
-
-
-
-
-
-
+ {#if userPreferences}
+
+
+
+
+
+
+
+
+
+ {/if}
@@ -210,15 +207,15 @@
{$t('uploads')}
- {#await getUserCalendarHeatmapAdmin({ ...getHeatmapRange(), id: user.id, $type: CalendarHeatmapType.Upload })}
-