1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

feat(web): improved server stats

This commit is contained in:
Michel Heusschen 2023-02-25 15:05:58 +01:00
parent 71d8567f18
commit 09a1d56bf3
25 changed files with 148 additions and 195 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -37,6 +37,7 @@ describe('Album service', () => {
shouldChangePassword: false, shouldChangePassword: false,
oauthId: '', oauthId: '',
tags: [], tags: [],
assets: [],
}); });
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const sharedAlbumOwnerId = '2222'; const sharedAlbumOwnerId = '2222';

View File

@ -2,28 +2,14 @@ import { ApiProperty } from '@nestjs/swagger';
import { UsageByUserDto } from './usage-by-user-response.dto'; import { UsageByUserDto } from './usage-by-user-response.dto';
export class ServerStatsResponseDto { export class ServerStatsResponseDto {
constructor() { @ApiProperty({ type: 'integer' })
this.photos = 0; photos = 0;
this.videos = 0;
this.usageByUser = [];
this.usageRaw = 0;
this.usage = '';
}
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
photos!: number; videos = 0;
@ApiProperty({ type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer' })
objects!: number;
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
usageRaw!: number; usage = 0;
@ApiProperty({ type: 'string' })
usage!: string;
@ApiProperty({ @ApiProperty({
isArray: true, isArray: true,
@ -37,5 +23,5 @@ export class ServerStatsResponseDto {
}, },
], ],
}) })
usageByUser!: UsageByUserDto[]; usageByUser: UsageByUserDto[] = [];
} }

View File

@ -1,22 +1,16 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class UsageByUserDto { export class UsageByUserDto {
constructor(userId: string) {
this.userId = userId;
this.videos = 0;
this.photos = 0;
this.usageRaw = 0;
this.usage = '0B';
}
@ApiProperty({ type: 'string' }) @ApiProperty({ type: 'string' })
userId: string; userId!: string;
@ApiProperty({ type: 'string' })
userFirstName!: string;
@ApiProperty({ type: 'string' })
userLastName!: string;
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
videos: number; photos!: number;
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
photos: number; videos!: number;
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
usageRaw!: number; usage!: number;
@ApiProperty({ type: 'string' })
usage!: string;
} }

View File

@ -1,11 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ServerInfoService } from './server-info.service'; import { ServerInfoService } from './server-info.service';
import { ServerInfoController } from './server-info.controller'; import { ServerInfoController } from './server-info.controller';
import { AssetEntity } from '@app/infra'; import { UserEntity } from '@app/infra';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([AssetEntity])], imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [ServerInfoController], controllers: [ServerInfoController],
providers: [ServerInfoService], providers: [ServerInfoService],
}) })

View File

@ -4,7 +4,7 @@ import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
import diskusage from 'diskusage'; import diskusage from 'diskusage';
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto'; import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
import { UsageByUserDto } from './response-dto/usage-by-user-response.dto'; import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
import { AssetEntity } from '@app/infra'; import { UserEntity } from '@app/infra';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { asHumanReadable } from '../../utils/human-readable.util'; import { asHumanReadable } from '../../utils/human-readable.util';
@ -12,8 +12,8 @@ import { asHumanReadable } from '../../utils/human-readable.util';
@Injectable() @Injectable()
export class ServerInfoService { export class ServerInfoService {
constructor( constructor(
@InjectRepository(AssetEntity) @InjectRepository(UserEntity)
private assetRepository: Repository<AssetEntity>, private userRepository: Repository<UserEntity>,
) {} ) {}
async getServerInfo(): Promise<ServerInfoResponseDto> { async getServerInfo(): Promise<ServerInfoResponseDto> {
@ -33,44 +33,48 @@ export class ServerInfoService {
} }
async getStats(): Promise<ServerStatsResponseDto> { async getStats(): Promise<ServerStatsResponseDto> {
const serverStats = new ServerStatsResponseDto();
type UserStatsQueryResponse = { type UserStatsQueryResponse = {
assetType: string; userId: string;
assetCount: string; userFirstName: string;
totalSizeInBytes: string; userLastName: string;
ownerId: string; photos: string;
videos: string;
usage: string;
}; };
const userStatsQueryResponse: UserStatsQueryResponse[] = await this.assetRepository const userStatsQueryResponse: UserStatsQueryResponse[] = await this.userRepository
.createQueryBuilder('a') .createQueryBuilder('users')
.select('COUNT(a.id)', 'assetCount') .select('users.id', 'userId')
.addSelect('SUM(ei.fileSizeInByte)', 'totalSizeInBytes') .addSelect('users.firstName', 'userFirstName')
.addSelect('a."ownerId"') .addSelect('users.lastName', 'userLastName')
.addSelect('a.type', 'assetType') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
.where('a.isVisible = true') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
.leftJoin('a.exifInfo', 'ei') .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage')
.groupBy('a."ownerId"') .leftJoin('users.assets', 'assets')
.addGroupBy('a.type') .leftJoin('assets.exifInfo', 'exif')
.groupBy('users.id')
.orderBy('users.createdAt', 'ASC')
.getRawMany(); .getRawMany();
const tmpMap = new Map<string, UsageByUserDto>(); const usageByUser = userStatsQueryResponse.map((userStats) => {
const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id); const usage = new UsageByUserDto();
userStatsQueryResponse.forEach((r) => { usage.userId = userStats.userId;
const usageByUser = getUsageByUser(r.ownerId); usage.userFirstName = userStats.userFirstName;
usageByUser.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0; usage.userLastName = userStats.userLastName;
usageByUser.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0; usage.photos = Number(userStats.photos);
usageByUser.usageRaw += parseInt(r.totalSizeInBytes); usage.videos = Number(userStats.videos);
usageByUser.usage = asHumanReadable(usageByUser.usageRaw); usage.usage = Number(userStats.usage);
serverStats.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0; return usage;
serverStats.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0;
serverStats.usageRaw += parseInt(r.totalSizeInBytes);
serverStats.usage = asHumanReadable(serverStats.usageRaw);
tmpMap.set(r.ownerId, usageByUser);
}); });
serverStats.usageByUser = Array.from(tmpMap.values()); const serverStats = new ServerStatsResponseDto();
usageByUser.forEach((user) => {
serverStats.photos += user.photos;
serverStats.videos += user.videos;
serverStats.usage += user.usage;
});
serverStats.usageByUser = usageByUser;
return serverStats; return serverStats;
} }

View File

@ -25,6 +25,7 @@ describe('TagService', () => {
deletedAt: undefined, deletedAt: undefined,
updatedAt: '2022-12-02T19:29:23.603Z', updatedAt: '2022-12-02T19:29:23.603Z',
tags: [], tags: [],
assets: [],
oauthId: 'oauth-id-1', oauthId: 'oauth-id-1',
}); });

View File

@ -4883,25 +4883,29 @@
"userId": { "userId": {
"type": "string" "type": "string"
}, },
"videos": { "userFirstName": {
"type": "integer" "type": "string"
},
"userLastName": {
"type": "string"
}, },
"photos": { "photos": {
"type": "integer" "type": "integer"
}, },
"usageRaw": { "videos": {
"type": "integer", "type": "integer"
"format": "int64"
}, },
"usage": { "usage": {
"type": "string" "type": "integer",
"format": "int64"
} }
}, },
"required": [ "required": [
"userId", "userId",
"videos", "userFirstName",
"userLastName",
"photos", "photos",
"usageRaw", "videos",
"usage" "usage"
] ]
}, },
@ -4909,22 +4913,20 @@
"type": "object", "type": "object",
"properties": { "properties": {
"photos": { "photos": {
"type": "integer" "type": "integer",
"default": 0
}, },
"videos": { "videos": {
"type": "integer"
},
"objects": {
"type": "integer"
},
"usageRaw": {
"type": "integer", "type": "integer",
"format": "int64" "default": 0
}, },
"usage": { "usage": {
"type": "string" "type": "integer",
"default": 0,
"format": "int64"
}, },
"usageByUser": { "usageByUser": {
"default": [],
"title": "Array of usage for each user", "title": "Array of usage for each user",
"example": [ "example": [
{ {
@ -4942,8 +4944,6 @@
"required": [ "required": [
"photos", "photos",
"videos", "videos",
"objects",
"usageRaw",
"usage", "usage",
"usageByUser" "usageByUser"
] ]

View File

@ -33,6 +33,7 @@ const adminUser: UserEntity = Object.freeze({
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01', updatedAt: '2021-01-01',
tags: [], tags: [],
assets: [],
}); });
const immichUser: UserEntity = Object.freeze({ const immichUser: UserEntity = Object.freeze({
@ -48,6 +49,7 @@ const immichUser: UserEntity = Object.freeze({
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01', updatedAt: '2021-01-01',
tags: [], tags: [],
assets: [],
}); });
const updatedImmichUser: UserEntity = Object.freeze({ const updatedImmichUser: UserEntity = Object.freeze({
@ -63,6 +65,7 @@ const updatedImmichUser: UserEntity = Object.freeze({
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01', updatedAt: '2021-01-01',
tags: [], tags: [],
assets: [],
}); });
const adminUserResponse = Object.freeze({ const adminUserResponse = Object.freeze({

View File

@ -76,6 +76,7 @@ export const userEntityStub = {
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01', updatedAt: '2021-01-01',
tags: [], tags: [],
assets: [],
}), }),
user1: Object.freeze<UserEntity>({ user1: Object.freeze<UserEntity>({
...authStub.user1, ...authStub.user1,
@ -88,6 +89,7 @@ export const userEntityStub = {
createdAt: '2021-01-01', createdAt: '2021-01-01',
updatedAt: '2021-01-01', updatedAt: '2021-01-01',
tags: [], tags: [],
assets: [],
}), }),
}; };

View File

@ -7,6 +7,7 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { AssetEntity } from './asset.entity';
import { TagEntity } from './tag.entity'; import { TagEntity } from './tag.entity';
@Entity('users') @Entity('users')
@ -49,4 +50,7 @@ export class UserEntity {
@OneToMany(() => TagEntity, (tag) => tag.user) @OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[]; tags!: TagEntity[];
@OneToMany(() => AssetEntity, (asset) => asset.owner)
assets!: AssetEntity[];
} }

View File

@ -1549,19 +1549,7 @@ export interface ServerStatsResponseDto {
* @type {number} * @type {number}
* @memberof ServerStatsResponseDto * @memberof ServerStatsResponseDto
*/ */
'objects': number; 'usage': number;
/**
*
* @type {number}
* @memberof ServerStatsResponseDto
*/
'usageRaw': number;
/**
*
* @type {string}
* @memberof ServerStatsResponseDto
*/
'usage': string;
/** /**
* *
* @type {Array<UsageByUserDto>} * @type {Array<UsageByUserDto>}
@ -2184,10 +2172,16 @@ export interface UsageByUserDto {
'userId': string; 'userId': string;
/** /**
* *
* @type {number} * @type {string}
* @memberof UsageByUserDto * @memberof UsageByUserDto
*/ */
'videos': number; 'userFirstName': string;
/**
*
* @type {string}
* @memberof UsageByUserDto
*/
'userLastName': string;
/** /**
* *
* @type {number} * @type {number}
@ -2199,13 +2193,13 @@ export interface UsageByUserDto {
* @type {number} * @type {number}
* @memberof UsageByUserDto * @memberof UsageByUserDto
*/ */
'usageRaw': number; 'videos': number;
/** /**
* *
* @type {string} * @type {number}
* @memberof UsageByUserDto * @memberof UsageByUserDto
*/ */
'usage': string; 'usage': number;
} }
/** /**
* *

View File

@ -1,43 +1,20 @@
<script lang="ts"> <script lang="ts">
import { api, ServerStatsResponseDto, UserResponseDto } from '@api'; import { ServerStatsResponseDto } from '@api';
import CameraIris from 'svelte-material-icons/CameraIris.svelte'; import CameraIris from 'svelte-material-icons/CameraIris.svelte';
import PlayCircle from 'svelte-material-icons/PlayCircle.svelte'; import PlayCircle from 'svelte-material-icons/PlayCircle.svelte';
import Memory from 'svelte-material-icons/Memory.svelte'; import Memory from 'svelte-material-icons/Memory.svelte';
import StatsCard from './stats-card.svelte'; import StatsCard from './stats-card.svelte';
import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units'; import { asByteUnitString, getBytesWithUnit } from '../../../utils/byte-units';
import { onMount, onDestroy } from 'svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
export let allUsers: Array<UserResponseDto>; export let stats: ServerStatsResponseDto = {
photos: 0,
let stats: ServerStatsResponseDto; videos: 0,
let setIntervalHandler: NodeJS.Timer; usage: 0,
usageByUser: []
onMount(async () => {
const { data } = await api.serverInfoApi.getStats();
stats = data;
setIntervalHandler = setInterval(async () => {
const { data } = await api.serverInfoApi.getStats();
stats = data;
}, 5000);
});
onDestroy(() => {
clearInterval(setIntervalHandler);
});
const getFullName = (userId: string) => {
let name = 'Admin'; // since we do not have admin user in allUsers
allUsers.forEach((user) => {
if (user.id === userId) name = `${user.firstName} ${user.lastName}`;
});
return name;
}; };
// Stats are unavailable if data is not loaded yet $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0);
$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats ? stats.usageRaw : 0);
</script> </script>
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
@ -45,14 +22,9 @@
<p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p> <p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p>
<div class="flex mt-5 justify-between"> <div class="flex mt-5 justify-between">
<StatsCard logo={CameraIris} title={'PHOTOS'} value={stats && stats.photos.toString()} /> <StatsCard logo={CameraIris} title="PHOTOS" value={stats.photos} />
<StatsCard logo={PlayCircle} title={'VIDEOS'} value={stats && stats.videos.toString()} /> <StatsCard logo={PlayCircle} title="VIDEOS" value={stats.videos} />
<StatsCard <StatsCard logo={Memory} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
logo={Memory}
title={'STORAGE'}
value={stats && spaceUsage.toString()}
unit={spaceUnit}
/>
</div> </div>
</div> </div>
@ -72,32 +44,19 @@
<tbody <tbody
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray dark:text-immich-dark-fg" class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray dark:text-immich-dark-fg"
> >
{#if stats} {#each stats.usageByUser as user (user.userId)}
{#each stats.usageByUser as user, i}
<tr
class={`text-center flex place-items-center w-full h-[50px] ${
i % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{getFullName(user.userId)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td
>
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td>
</tr>
{/each}
{:else}
<tr <tr
class="text-center flex place-items-center w-full h-[50px] bg-immich-gray dark:bg-immich-dark-gray/75" class="text-center flex place-items-center w-full h-[50px] even:bg-immich-bg even:dark:bg-immich-dark-gray/50 odd:bg-immich-gray odd:dark:bg-immich-dark-gray/75"
> >
<td class="w-full flex justify-center"> <td class="text-sm px-2 w-1/4 text-ellipsis"
<LoadingSpinner /> >{user.userFirstName} {user.userLastName}</td
</td> >
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td>
<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usage, $locale)}</td
>
</tr> </tr>
{/if} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -1,19 +1,14 @@
<script lang="ts"> <script lang="ts">
import type Icon from 'svelte-material-icons/AbTesting.svelte'; import type Icon from 'svelte-material-icons/AbTesting.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
export let logo: typeof Icon; export let logo: typeof Icon;
export let title: string; export let title: string;
export let value: string; export let value: number;
export let unit: string | undefined = undefined; export let unit: string | undefined = undefined;
$: zeros = () => { $: zeros = () => {
if (!value) {
return '';
}
const maxLength = 13; const maxLength = 13;
const valueLength = parseInt(value).toString().length; const valueLength = value.toString().length;
const zeroLength = maxLength - valueLength; const zeroLength = maxLength - valueLength;
return '0'.repeat(zeroLength); return '0'.repeat(zeroLength);
@ -29,15 +24,9 @@
</div> </div>
<div class="relative text-center font-mono font-semibold text-2xl"> <div class="relative text-center font-mono font-semibold text-2xl">
{#if value !== undefined} <span class="text-[#DCDADA] dark:text-[#525252]">{zeros()}</span><span
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros()}</span><span class="text-immich-primary dark:text-immich-dark-primary">{value}</span
class="text-immich-primary dark:text-immich-dark-primary">{parseInt(value)}</span >
>
{:else}
<div class="flex justify-end pr-2">
<LoadingSpinner />
</div>
{/if}
{#if unit} {#if unit}
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span> <span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span>
{/if} {/if}

View File

@ -135,7 +135,7 @@
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p> <p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>
{/if} {/if}
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte)}</p> <p>{asByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@
import LoadingSpinner from './loading-spinner.svelte'; import LoadingSpinner from './loading-spinner.svelte';
import { api, ServerInfoResponseDto } from '@api'; import { api, ServerInfoResponseDto } from '@api';
import { asByteUnitString } from '../../utils/byte-units'; import { asByteUnitString } from '../../utils/byte-units';
import { locale } from '$lib/stores/preferences.store';
let isServerOk = true; let isServerOk = true;
let serverVersion = ''; let serverVersion = '';
@ -63,7 +64,8 @@
/> />
</div> </div>
<p class="text-xs"> <p class="text-xs">
{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used {asByteUnitString(serverInfo?.diskUseRaw, $locale)} of
{asByteUnitString(serverInfo?.diskSizeRaw, $locale)} used
</p> </p>
{:else} {:else}
<div class="mt-2"> <div class="mt-2">

View File

@ -3,6 +3,7 @@
import { asByteUnitString } from '$lib/utils/byte-units'; import { asByteUnitString } from '$lib/utils/byte-units';
import { UploadAsset } from '$lib/models/upload-asset'; import { UploadAsset } from '$lib/models/upload-asset';
import ImmichLogo from './immich-logo.svelte'; import ImmichLogo from './immich-logo.svelte';
import { locale } from '$lib/stores/preferences.store';
export let uploadAsset: UploadAsset; export let uploadAsset: UploadAsset;
@ -50,7 +51,7 @@
<input <input
disabled disabled
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2" class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
value={`[${asByteUnitString(uploadAsset.file.size)}] ${uploadAsset.file.name}`} value={`[${asByteUnitString(uploadAsset.file.size, $locale)}] ${uploadAsset.file.name}`}
/> />
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative"> <div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">

View File

@ -38,8 +38,7 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, stri
* @param maxPrecision maximum number of decimal places, default is `1` * @param maxPrecision maximum number of decimal places, default is `1`
* @returns localized bytes with unit as string * @returns localized bytes with unit as string
*/ */
export function asByteUnitString(bytes: number, maxPrecision = 1): string { export function asByteUnitString(bytes: number, locale?: string, maxPrecision = 1): string {
const locale = Array.from(navigator.languages);
const [size, unit] = getBytesWithUnit(bytes, maxPrecision); const [size, unit] = getBytesWithUnit(bytes, maxPrecision);
return `${size.toLocaleString(locale)} ${unit}`; return `${size.toLocaleString(locale)} ${unit}`;
} }

View File

@ -10,12 +10,12 @@ export const load = (async ({ parent, locals: { api } }) => {
throw redirect(302, '/photos'); throw redirect(302, '/photos');
} }
const { data: allUsers } = await api.userApi.getAllUsers(false); const { data: stats } = await api.serverInfoApi.getStats();
return { return {
allUsers, stats,
meta: { meta: {
title: 'Server Status' title: 'Server Stats'
} }
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View File

@ -1,8 +1,22 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { api } from '@api';
import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte'; import ServerStatsPanel from '$lib/components/admin-page/server-stats/server-stats-panel.svelte';
import { page } from '$app/stores'; import type { PageData } from './$types';
export let data: PageData;
let setIntervalHandler: NodeJS.Timer;
onMount(async () => {
setIntervalHandler = setInterval(async () => {
const { data: stats } = await api.serverInfoApi.getStats();
data.stats = stats;
}, 5000);
});
onDestroy(() => {
clearInterval(setIntervalHandler);
});
</script> </script>
{#if $page.data.allUsers} <ServerStatsPanel stats={data.stats} />
<ServerStatsPanel allUsers={$page.data.allUsers} />
{/if}