refactor: user admin queries (#32490)
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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 } }]);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user