1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-04 02:46:47 +01:00

Merge branch 'main' of github.com:immich-app/immich

This commit is contained in:
Alex Tran 2023-02-26 13:59:48 -06:00
commit d04ef319b8
No known key found for this signature in database
GPG key ID: E4954BC787B85C8A
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

@ -54,6 +54,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({
@ -69,6 +70,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({
@ -84,6 +86,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}