refactor: server endpoints (#32467)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ByteUnit } from '$lib/utils/byte-units';
|
||||
import { Icon, Text, type MaybePromise } from '@immich/ui';
|
||||
import { Icon, Text } from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type ValueData = {
|
||||
@@ -11,12 +11,12 @@
|
||||
interface Props {
|
||||
icon?: string;
|
||||
title: string;
|
||||
valuePromise: MaybePromise<ValueData>;
|
||||
value?: ValueData;
|
||||
tooltip?: string;
|
||||
footer?: Snippet;
|
||||
}
|
||||
|
||||
let { icon, title, valuePromise, tooltip, footer }: Props = $props();
|
||||
let { icon, title, value, tooltip, footer }: Props = $props();
|
||||
const zeros = (data?: ValueData) => {
|
||||
let length = 13;
|
||||
if (data) {
|
||||
@@ -36,22 +36,18 @@
|
||||
<Text size="giant" fontWeight="medium" title={tooltip}>{title}</Text>
|
||||
</div>
|
||||
|
||||
{#await valuePromise}
|
||||
{#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}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="0">
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros()}</span>
|
||||
</div>
|
||||
{:then data}
|
||||
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="{data.value} {data.unit ?? ''}">
|
||||
<span class="text-gray-300 dark:text-gray-600">{zeros(data)}</span><span>{data.value}</span>
|
||||
{#if data.unit}
|
||||
<code class="font-mono text-base font-normal">{data.unit}</code>
|
||||
{/if}
|
||||
</div>
|
||||
{:catch _}
|
||||
<div class="relative mx-auto font-mono text-2xl font-medium" aria-label="0">
|
||||
<span class="text-gray-300 dark:text-gray-600">{zeros()}</span>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
{@render footer?.()}
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
import AvatarEditModal from '$lib/modals/AvatarEditModal.svelte';
|
||||
import HelpAndFeedbackModal from '$lib/modals/HelpAndFeedbackModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { getAboutInfoQuery } from '$lib/queries/server.query';
|
||||
import { Button, Icon, IconButton, modalManager } from '@immich/ui';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import UserAvatar from '../UserAvatar.svelte';
|
||||
@@ -20,11 +19,8 @@
|
||||
|
||||
let { onClose }: Props = $props();
|
||||
|
||||
let info: ServerAboutResponseDto | undefined = $state();
|
||||
|
||||
onMount(async () => {
|
||||
info = userInteraction.aboutInfo ?? (await getAboutInfo());
|
||||
});
|
||||
const query = createQuery(() => getAboutInfoQuery());
|
||||
const info = $derived(query.data);
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -2,38 +2,21 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { releaseManager } from '$lib/managers/release-manager.svelte';
|
||||
import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getAboutInfoQuery, getVersionHistoryQuery } from '$lib/queries/server.query';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { semverToName } from '$lib/utils';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import {
|
||||
getAboutInfo,
|
||||
getVersionHistory,
|
||||
type ReleaseEventV1,
|
||||
type ServerAboutResponseDto,
|
||||
type ServerVersionHistoryResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { type ReleaseEventV1 } from '@immich/sdk';
|
||||
import { Icon, modalManager, Text } from '@immich/ui';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { mdiAlert, mdiNewBox } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const { serverVersion, connected } = websocketStore;
|
||||
|
||||
let info: ServerAboutResponseDto | undefined = $state();
|
||||
let versions: ServerVersionHistoryResponseDto[] = $state([]);
|
||||
|
||||
onMount(async () => {
|
||||
if (userInteraction.aboutInfo && userInteraction.versions && $serverVersion) {
|
||||
info = userInteraction.aboutInfo;
|
||||
versions = userInteraction.versions;
|
||||
return;
|
||||
}
|
||||
await requestServerInfo();
|
||||
[info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]);
|
||||
userInteraction.aboutInfo = info;
|
||||
userInteraction.versions = versions;
|
||||
});
|
||||
const aboutQuery = createQuery(() => getAboutInfoQuery());
|
||||
const info = $derived(aboutQuery.data);
|
||||
const historyQuery = createQuery(() => getVersionHistoryQuery());
|
||||
const versions = $derived(historyQuery.data ?? []);
|
||||
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
|
||||
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
|
||||
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import { getStorageQuery } from '$lib/queries/server.query';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { LoadingSpinner, Meter } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const query = createQuery(() => getStorageQuery());
|
||||
const serverInfo = $derived(query.data);
|
||||
|
||||
let hasQuota = $derived(authManager.user.quotaSizeInBytes !== null);
|
||||
let availableBytes = $derived(
|
||||
(hasQuota && authManager.authenticated
|
||||
? authManager.user.quotaSizeInBytes
|
||||
: userInteraction.serverInfo?.diskSizeRaw) || 0,
|
||||
(hasQuota && authManager.authenticated ? authManager.user.quotaSizeInBytes : serverInfo?.diskSizeRaw) || 0,
|
||||
);
|
||||
let usedBytes = $derived(
|
||||
(hasQuota && authManager.authenticated
|
||||
? authManager.user.quotaUsageInBytes
|
||||
: userInteraction.serverInfo?.diskUseRaw) || 0,
|
||||
(hasQuota && authManager.authenticated ? authManager.user.quotaUsageInBytes : serverInfo?.diskUseRaw) || 0,
|
||||
);
|
||||
|
||||
const thresholds = [
|
||||
{ from: 0.8, className: 'bg-warning' },
|
||||
{ from: 0.95, className: 'bg-danger' },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
if (userInteraction.serverInfo && authManager.authenticated) {
|
||||
return;
|
||||
}
|
||||
await requestServerInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -42,7 +33,7 @@
|
||||
},
|
||||
})}
|
||||
>
|
||||
{#if userInteraction.serverInfo}
|
||||
{#if serverInfo}
|
||||
<Meter
|
||||
size="tiny"
|
||||
class="bg-light-200"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
getAboutInfo,
|
||||
getMyPreferences,
|
||||
getMyUser,
|
||||
logout,
|
||||
@@ -10,6 +9,8 @@ import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { getAboutInfoQuery } from '$lib/queries/server.query';
|
||||
import { Route } from '$lib/route';
|
||||
import { isSharedLinkRoute } from '$lib/utils/navigation';
|
||||
|
||||
@@ -69,7 +70,7 @@ class AuthManager {
|
||||
this.isPurchased = true;
|
||||
} else {
|
||||
// check server status
|
||||
const serverInfo = await getAboutInfo().catch(() => {});
|
||||
const serverInfo = await queryClient.query(getAboutInfoQuery()).catch(() => {});
|
||||
if (serverInfo?.licensed) {
|
||||
this.isPurchased = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getLibraryStatistics } from '@immich/sdk';
|
||||
import { queryOptions } from '@tanstack/svelte-query';
|
||||
|
||||
export const getLibraryStatisticsQuery = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['libraries', id, 'statistics'],
|
||||
queryFn: ({ signal }) => getLibraryStatistics({ id }, { signal }),
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getAboutInfo, getServerStatistics, getStorage, getVersionHistory } from '@immich/sdk';
|
||||
import { queryOptions } from '@tanstack/svelte-query';
|
||||
|
||||
export const getAboutInfoQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['server', 'about'],
|
||||
queryFn: ({ signal }) => getAboutInfo({ signal }),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
export const getVersionHistoryQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['server', 'version-history'],
|
||||
queryFn: ({ signal }) => getVersionHistory({ signal }),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
export const getStorageQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['server', 'storage'],
|
||||
queryFn: ({ signal }) => getStorage({ signal }),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
export const getServerStatisticsQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['server', 'statistics'],
|
||||
queryFn: ({ signal }) => getServerStatistics({ signal }),
|
||||
});
|
||||
@@ -1,23 +1,12 @@
|
||||
import type {
|
||||
AlbumResponseDto,
|
||||
ServerAboutResponseDto,
|
||||
ServerStorageResponseDto,
|
||||
ServerVersionHistoryResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
|
||||
interface UserInteractions {
|
||||
recentAlbums?: AlbumResponseDto[];
|
||||
versions?: ServerVersionHistoryResponseDto[];
|
||||
aboutInfo?: ServerAboutResponseDto;
|
||||
serverInfo?: ServerStorageResponseDto;
|
||||
}
|
||||
|
||||
const defaultUserInteraction: UserInteractions = {
|
||||
recentAlbums: undefined,
|
||||
versions: undefined,
|
||||
aboutInfo: undefined,
|
||||
serverInfo: undefined,
|
||||
};
|
||||
|
||||
export const userInteraction = $state<UserInteractions>(defaultUserInteraction);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type SyncAssetEditV1,
|
||||
type SyncAssetV2,
|
||||
} from '@immich/sdk';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { page } from '$app/state';
|
||||
@@ -69,7 +70,13 @@ websocket
|
||||
websocketStore.connected.set(true);
|
||||
})
|
||||
.on('disconnect', () => websocketStore.connected.set(false))
|
||||
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
|
||||
.on('on_server_version', (serverVersion) => {
|
||||
const previous = get(websocketStore.serverVersion);
|
||||
websocketStore.serverVersion.set(serverVersion);
|
||||
if (previous && !isEqual(previous, serverVersion)) {
|
||||
void queryClient.invalidateQueries({ queryKey: ['server'] });
|
||||
}
|
||||
})
|
||||
.on(
|
||||
'on_config_update',
|
||||
() => void queryClient.invalidateQueries({ queryKey: getConfigQuery().queryKey, exact: true }),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { getStorage } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { DateTime } from 'luxon';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
|
||||
export interface AuthOptions {
|
||||
admin?: true;
|
||||
@@ -27,15 +25,6 @@ export const authenticate = async (url: URL, options?: AuthOptions) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const requestServerInfo = async () => {
|
||||
if (!authManager.authenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await getStorage();
|
||||
userInteraction.serverInfo = data;
|
||||
};
|
||||
|
||||
export const getAccountAge = (): number => {
|
||||
if (!authManager.authenticated) {
|
||||
return 0;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/publ
|
||||
import type { ImmichProduct } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { getAboutInfoQuery } from '$lib/queries/server.query';
|
||||
|
||||
export const activateProduct = async (licenseKey: string, activationKey: string): Promise<LicenseResponseDto> => {
|
||||
// TODO is this needed?
|
||||
@@ -11,7 +13,14 @@ export const activateProduct = async (licenseKey: string, activationKey: string)
|
||||
const isServerActivation = authManager.user.isAdmin && licenseKey.search('IMSV') !== -1;
|
||||
const licenseKeyDto = { licenseKey, activationKey };
|
||||
// Send server key to user activation if user is not admin
|
||||
return isServerActivation ? setServerLicense({ licenseKeyDto }) : setUserLicense({ licenseKeyDto });
|
||||
if (!isServerActivation) {
|
||||
return setUserLicense({ licenseKeyDto });
|
||||
}
|
||||
|
||||
const license = await setServerLicense({ licenseKeyDto });
|
||||
// contains license status
|
||||
await queryClient.invalidateQueries({ queryKey: getAboutInfoQuery().queryKey });
|
||||
return license;
|
||||
};
|
||||
|
||||
export const getActivationKey = async (licenseKey: string): Promise<string> => {
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/SettingSwitch.svelte';
|
||||
import { dateFormats } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAboutInfoQuery } from '$lib/queries/server.query';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
|
||||
import {
|
||||
deleteUserLicense as deleteIndividualProductKey,
|
||||
deleteServerLicense as deleteServerProductKey,
|
||||
getAboutInfo,
|
||||
getMyUser,
|
||||
getServerLicense,
|
||||
isHttpError,
|
||||
@@ -26,7 +27,7 @@
|
||||
let serverPurchaseInfo: LicenseResponseDto | null = $state(null);
|
||||
|
||||
const checkPurchaseInfo = async () => {
|
||||
const serverInfo = await getAboutInfo();
|
||||
const serverInfo = await queryClient.query(getAboutInfoQuery());
|
||||
isServerProduct = serverInfo.licensed;
|
||||
|
||||
const response = await getMyUser();
|
||||
@@ -90,6 +91,8 @@
|
||||
}
|
||||
|
||||
await deleteServerProductKey();
|
||||
// contains license status
|
||||
await queryClient.invalidateQueries({ queryKey: getAboutInfoQuery().queryKey });
|
||||
authManager.isPurchased = false;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_remove_product_key'));
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { getAllLibraries, getLibraryStatistics, getUserAdmin, searchUsersAdmin } from '@immich/sdk';
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url, depends }) => {
|
||||
depends('app:libraries');
|
||||
await authenticate(url, { admin: true });
|
||||
await requestServerInfo();
|
||||
const allUsers = await searchUsersAdmin({ withDeleted: false });
|
||||
const $t = await getFormatter();
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/EmptyPlaceholder.svelte';
|
||||
import TableButton from '$lib/components/TableButton.svelte';
|
||||
import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { getLibraryStatisticsQuery } from '$lib/queries/library.query';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
getLibraryActions,
|
||||
@@ -16,6 +18,7 @@
|
||||
} from '$lib/services/library.service';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { Code, CommandPaletteDefaultProvider, Container, Heading, modalManager } from '@immich/ui';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { mdiCameraIris, mdiChartPie, mdiFilterMinusOutline, mdiFolderOutline, mdiPlayCircle } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -28,20 +31,25 @@
|
||||
|
||||
const { children, data }: Props = $props();
|
||||
|
||||
const photosPromise = $derived(data.statisticsPromise.then((stats) => ({ value: stats.photos })));
|
||||
|
||||
const videosPromise = $derived(data.statisticsPromise.then((stats) => ({ value: stats.videos })));
|
||||
|
||||
const usagePromise = $derived(
|
||||
data.statisticsPromise.then((stats) => {
|
||||
const [value, unit] = getBytesWithUnit(stats.usage);
|
||||
return { value, unit };
|
||||
}),
|
||||
);
|
||||
|
||||
const library = $derived(data.library);
|
||||
|
||||
const onLibraryUpdate = () => invalidate('app:library');
|
||||
const query = createQuery(() => getLibraryStatisticsQuery(library.id));
|
||||
const stats = $derived(query.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 onLibraryUpdate = async () => {
|
||||
await invalidate('app:library');
|
||||
await queryClient.invalidateQueries({ queryKey: getLibraryStatisticsQuery(library.id).queryKey });
|
||||
};
|
||||
|
||||
const onLibraryDelete = async ({ id }: { id: string }) => {
|
||||
if (id === library.id) {
|
||||
@@ -64,9 +72,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')} valuePromise={photosPromise} />
|
||||
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} valuePromise={videosPromise} />
|
||||
<ServerStatisticsCard icon={mdiChartPie} title={$t('usage')} valuePromise={usagePromise} />
|
||||
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={photos} />
|
||||
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={videos} />
|
||||
<ServerStatisticsCard icon={mdiChartPie} title={$t('usage')} value={usage} />
|
||||
</div>
|
||||
|
||||
<AdminCard icon={mdiFolderOutline} title={$t('folders')} headerAction={AddFolder}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
|
||||
import { getLibrary, type LibraryResponseDto } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { Route } from '$lib/route';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
@@ -17,12 +17,10 @@ export const load = (async ({ params: { id }, url, depends }) => {
|
||||
redirect(307, Route.libraries());
|
||||
}
|
||||
|
||||
const statisticsPromise = getLibraryStatistics({ id });
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
library,
|
||||
statisticsPromise,
|
||||
meta: {
|
||||
title: $t('admin.library_details'),
|
||||
},
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<ServerStatisticsCard
|
||||
title={$t(getReportTypeTranslation(reportType))}
|
||||
tooltip={$t(getReportTypeDescriptionKey(reportType))}
|
||||
valuePromise={{ value: integrityReport[reportType] }}
|
||||
value={{ value: integrityReport[reportType] }}
|
||||
>
|
||||
{#snippet footer()}
|
||||
<HStack gap={1} class="justify-between">
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { getQueue, getQueueJobs, QueueJobStatus } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { fromQueueSlug, Route } from '$lib/route';
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
await requestServerInfo();
|
||||
|
||||
const name = fromQueueSlug(params.name);
|
||||
if (!name) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import ServerStatisticsPanel from './ServerStatisticsPanel.svelte';
|
||||
import { getServerStatistics, type ServerStatsResponseDto } from '@immich/sdk';
|
||||
import { Container } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
type Props = {
|
||||
@@ -11,29 +9,10 @@
|
||||
};
|
||||
|
||||
const { data }: Props = $props();
|
||||
|
||||
let stats = $state<ServerStatsResponseDto | undefined>(undefined);
|
||||
|
||||
const statsPromise = $derived.by(() => {
|
||||
if (stats) {
|
||||
return Promise.resolve(stats);
|
||||
}
|
||||
return data.statsPromise;
|
||||
});
|
||||
|
||||
const updateStatistics = async () => {
|
||||
stats = await getServerStatistics();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => void updateStatistics(), 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
<Container size="large" center>
|
||||
<ServerStatisticsPanel {statsPromise} users={data.users} />
|
||||
<ServerStatisticsPanel users={data.users} />
|
||||
</Container>
|
||||
</AdminPageLayout>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { getServerStatistics, searchUsersAdmin } from '@immich/sdk';
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import { noop } from 'lodash-es';
|
||||
import { queryClient } from '$lib/queries/client';
|
||||
import { getServerStatisticsQuery } from '$lib/queries/server.query';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
const statsPromise = getServerStatistics();
|
||||
void queryClient.query(getServerStatisticsQuery()).catch(noop);
|
||||
const users = await searchUsersAdmin({ withDeleted: false });
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
statsPromise,
|
||||
users,
|
||||
meta: {
|
||||
title: $t('server_stats'),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
||||
import { getServerStatisticsQuery } from '$lib/queries/server.query';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import type { ServerStatsResponseDto, UserAdminResponseDto } from '@immich/sdk';
|
||||
import type { UserAdminResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
Code,
|
||||
FormatBytes,
|
||||
@@ -15,33 +16,34 @@
|
||||
TableRow,
|
||||
Text,
|
||||
} from '@immich/ui';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
statsPromise: Promise<ServerStatsResponseDto>;
|
||||
users: UserAdminResponseDto[];
|
||||
};
|
||||
|
||||
const { statsPromise, users }: Props = $props();
|
||||
const { users }: Props = $props();
|
||||
|
||||
const photosPromise = $derived.by(() => statsPromise.then((data) => ({ value: data.photos })));
|
||||
|
||||
const videosPromise = $derived.by(() => statsPromise.then((data) => ({ value: data.videos })));
|
||||
|
||||
const storagePromise = $derived.by(() =>
|
||||
statsPromise.then((data) => {
|
||||
const TiB = 1024 ** 4;
|
||||
const [value, unit] = getBytesWithUnit(data.usage, data.usage > TiB ? 2 : 0);
|
||||
return { value, unit };
|
||||
}),
|
||||
);
|
||||
const query = createQuery(() => ({ ...getServerStatisticsQuery(), refetchInterval: 5000 }));
|
||||
const stats = $derived(query.data);
|
||||
|
||||
const getStorageUsageWithUnit = (usage: number) => {
|
||||
const TiB = 1024 ** 4;
|
||||
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 zeros = (value: number, maxLength = 13) => {
|
||||
const valueLength = value.toString().length;
|
||||
const zeroLength = maxLength - valueLength;
|
||||
@@ -49,10 +51,7 @@
|
||||
return '0'.repeat(zeroLength);
|
||||
};
|
||||
|
||||
const getUserStatsPromise = async (userId: string) => {
|
||||
const stats = await statsPromise;
|
||||
return stats.usageByUser.find((userStats) => userStats.userId === userId);
|
||||
};
|
||||
const getUserStats = (userId: string) => stats?.usageByUser.find((userStats) => userStats.userId === userId);
|
||||
</script>
|
||||
|
||||
{#snippet placeholder()}
|
||||
@@ -66,9 +65,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')} valuePromise={photosPromise} />
|
||||
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} valuePromise={videosPromise} />
|
||||
<ServerStatisticsCard icon={mdiChartPie} title={$t('storage')} valuePromise={storagePromise} />
|
||||
<ServerStatisticsCard icon={mdiCameraIris} title={$t('photos')} value={photos} />
|
||||
<ServerStatisticsCard icon={mdiPlayCircle} title={$t('videos')} value={videos} />
|
||||
<ServerStatisticsCard icon={mdiChartPie} title={$t('storage')} value={storage} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex lg:hidden">
|
||||
@@ -80,13 +79,11 @@
|
||||
</div>
|
||||
|
||||
<div class="relative text-center font-mono text-2xl font-medium">
|
||||
{#await statsPromise}
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{:then stats}
|
||||
{#if stats}
|
||||
<span class="text-light-300">{zeros(stats.photos)}</span><span class="text-primary">{stats.photos}</span>
|
||||
{:catch}
|
||||
<span class="text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{/await}
|
||||
{:else}
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-12">
|
||||
@@ -96,13 +93,11 @@
|
||||
</div>
|
||||
|
||||
<div class="relative text-center font-mono text-2xl font-medium">
|
||||
{#await statsPromise}
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{:then stats}
|
||||
{#if stats}
|
||||
<span class="text-light-300">{zeros(stats.videos)}</span><span class="text-primary">{stats.videos}</span>
|
||||
{:catch}
|
||||
<span class="text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{/await}
|
||||
{:else}
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-5">
|
||||
@@ -112,9 +107,7 @@
|
||||
</div>
|
||||
|
||||
<div class="relative flex text-center font-mono text-2xl font-medium">
|
||||
{#await statsPromise}
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{:then stats}
|
||||
{#if stats}
|
||||
{@const storageUsageWithUnit = getStorageUsageWithUnit(stats.usage)}
|
||||
<span class="text-light-300">{zeros(storageUsageWithUnit[0])}</span><span class="text-primary"
|
||||
>{storageUsageWithUnit[0]}</span
|
||||
@@ -123,9 +116,9 @@
|
||||
<div class="absolute -inset-e-1.5 -bottom-4">
|
||||
<Code color="muted" class="font-mono text-xs font-light">{storageUsageWithUnit[1]}</Code>
|
||||
</div>
|
||||
{:catch}
|
||||
<span class="text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{/await}
|
||||
{:else}
|
||||
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros(0)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,9 +138,7 @@
|
||||
{#each users as user (user.id)}
|
||||
<TableRow>
|
||||
<TableCell class="w-1/4">{user.name}</TableCell>
|
||||
{#await getUserStatsPromise(user.id)}
|
||||
{@render placeholder()}
|
||||
{:then userStats}
|
||||
{@const userStats = getUserStats(user.id)}
|
||||
{#if userStats}
|
||||
<TableCell class="w-1/4">
|
||||
{userStats.photos.toLocaleString($locale)} (<FormatBytes bytes={userStats.usagePhotos} />)</TableCell
|
||||
@@ -180,7 +171,6 @@
|
||||
{:else}
|
||||
{@render placeholder()}
|
||||
{/if}
|
||||
{/await}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
let { config = $bindable() }: Props = $props();
|
||||
|
||||
const saved = createDefinedQuery(getConfigQuery());
|
||||
const query = createDefinedQuery(getConfigQuery());
|
||||
|
||||
let loadingPreview = $state(false);
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
];
|
||||
|
||||
const isEdited = (templateKey: keyof AdminConfigTemplateEmailsDto) =>
|
||||
config.templates.email[templateKey] !== saved.data.templates.email[templateKey];
|
||||
config.templates.email[templateKey] !== query.data.templates.email[templateKey];
|
||||
|
||||
const onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { searchUsersAdmin } from '@immich/sdk';
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
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 });
|
||||
await requestServerInfo();
|
||||
const users = await searchUsersAdmin({ withDeleted: true });
|
||||
const $t = await getFormatter();
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleCreateUserAdmin } from '$lib/services/user-admin.service';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getStorageQuery } from '$lib/queries/server.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';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const query = createQuery(() => getStorageQuery());
|
||||
const serverInfo = $derived(query.data);
|
||||
|
||||
let success = $state(false);
|
||||
|
||||
let email = $state('');
|
||||
@@ -22,9 +26,7 @@
|
||||
let isCreatingUser = $state(false);
|
||||
|
||||
let quotaSizeInBytes = $derived(quotaSize === null ? null : convertToBytes(Number(quotaSize), ByteUnit.GiB));
|
||||
let quotaSizeWarning = $derived(
|
||||
quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
|
||||
);
|
||||
let quotaSizeWarning = $derived(quotaSizeInBytes && serverInfo && quotaSizeInBytes > serverInfo.diskSizeRaw);
|
||||
|
||||
const passwordMismatch = $derived(password !== passwordConfirm && passwordConfirm.length > 0);
|
||||
const passwordMismatchMessage = $derived(passwordMismatch ? $t('password_does_not_match') : '');
|
||||
|
||||
@@ -122,20 +122,12 @@
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<div class="flex w-full flex-col gap-4 lg:flex-row">
|
||||
<ServerStatisticsCard
|
||||
icon={mdiCameraIris}
|
||||
title={$t('photos')}
|
||||
valuePromise={Promise.resolve({ value: userStatistics.images })}
|
||||
/>
|
||||
<ServerStatisticsCard
|
||||
icon={mdiPlayCircle}
|
||||
title={$t('videos')}
|
||||
valuePromise={Promise.resolve({ value: userStatistics.videos })}
|
||||
/>
|
||||
<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')}
|
||||
valuePromise={Promise.resolve({ value: statsUsage, unit: statsUsageUnit })}
|
||||
value={{ value: statsUsage, unit: statsUsageUnit }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,12 @@ import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin,
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { UUID_REGEX } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
await requestServerInfo();
|
||||
|
||||
if (!UUID_REGEX.test(params.id)) {
|
||||
redirect(307, Route.users());
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleUpdateUserAdmin } from '$lib/services/user-admin.service';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getStorageQuery } from '$lib/queries/server.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';
|
||||
@@ -16,6 +17,9 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const query = createQuery(() => getStorageQuery());
|
||||
const serverInfo = $derived(query.data);
|
||||
|
||||
const user = $derived(data.user);
|
||||
let { isAdmin, name, email } = $derived(user);
|
||||
let storageLabel = $derived(user.storageLabel || '');
|
||||
@@ -28,10 +32,7 @@
|
||||
const quotaSizeBytes = $derived(typeof quotaSize === 'number' ? convertToBytes(quotaSize, ByteUnit.GiB) : null);
|
||||
|
||||
let quotaSizeWarning = $derived(
|
||||
previousQuota !== quotaSizeBytes &&
|
||||
!!quotaSizeBytes &&
|
||||
userInteraction.serverInfo &&
|
||||
quotaSizeBytes > userInteraction.serverInfo.diskSizeRaw,
|
||||
previousQuota !== quotaSizeBytes && !!quotaSizeBytes && serverInfo && quotaSizeBytes > serverInfo.diskSizeRaw,
|
||||
);
|
||||
|
||||
const onClose = async () => {
|
||||
|
||||
Reference in New Issue
Block a user