From 09a1d56bf3a7412ab6cc7e5b956628252db63ab9 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 25 Feb 2023 15:05:58 +0100 Subject: [PATCH] feat(web): improved server stats --- mobile/openapi/doc/ServerStatsResponseDto.md | Bin 619 -> 601 bytes mobile/openapi/doc/UsageByUserDto.md | Bin 522 -> 563 bytes .../lib/model/server_stats_response_dto.dart | Bin 4677 -> 4184 bytes .../openapi/lib/model/usage_by_user_dto.dart | Bin 4206 -> 4562 bytes .../test/server_stats_response_dto_test.dart | Bin 1113 -> 973 bytes .../openapi/test/usage_by_user_dto_test.dart | Bin 943 -> 1062 bytes .../src/api-v1/album/album.service.spec.ts | 1 + .../response-dto/server-stats-response.dto.ts | 24 ++--- .../usage-by-user-response.dto.ts | 22 ++--- .../api-v1/server-info/server-info.module.ts | 4 +- .../api-v1/server-info/server-info.service.ts | 70 ++++++++------- .../immich/src/api-v1/tag/tag.service.spec.ts | 1 + server/immich-openapi-specs.json | 38 ++++---- .../libs/domain/src/user/user.service.spec.ts | 3 + server/libs/domain/test/fixtures.ts | 2 + .../libs/infra/src/db/entities/user.entity.ts | 4 + web/src/api/open-api/api.ts | 30 +++---- .../server-stats/server-stats-panel.svelte | 83 +++++------------- .../admin-page/server-stats/stats-card.svelte | 21 ++--- .../asset-viewer/detail-panel.svelte | 2 +- .../shared-components/status-box.svelte | 4 +- .../upload-asset-preview.svelte | 3 +- web/src/lib/utils/byte-units.ts | 3 +- .../admin/server-status/+page.server.ts | 6 +- .../routes/admin/server-status/+page.svelte | 22 ++++- 25 files changed, 148 insertions(+), 195 deletions(-) diff --git a/mobile/openapi/doc/ServerStatsResponseDto.md b/mobile/openapi/doc/ServerStatsResponseDto.md index 3356c366f245a5955e0a0d26e908623328a5a6cb..96446e1c28dc67dacf736191caeef1bb2dbdc02a 100644 GIT binary patch delta 84 zcmaFOa+77kPrm4s)U?FXoDzkSdB$a^(j2A5iRr09iRF_M85Ji!8H|uj>VVkTZqznMh$s-5= delta 468 zcmcbia8zZ(G{*X()WXutqSO?Hl8nq^y@HJVlKf&FE(IV!l_|?iNyRRcpOlrFT!K|o zX>nqDYEWW1HUq(OI+M>c`YJlX~0!2WuBt~Rb>lf=)euJRe>wC zXF1~tRj3}TU<(N+sDL%tPz8mo;{3emB6XM^ggThW=07Y>ENl>6lX*DhWnd<1Dnh~r m6yPw$lX*GiIN%&ZPGvSo7;FybyvHUE(+SZBw{x_*zE+;$2%6 z2tB!vr8gACH1${oTSTbCqs1EHU4^XT{JiKQb!3O4>VVli*^t#_vNWse=04UH%sj|e zOn%I%qKp*d>Y9p>NC!nclEEN#Tu2e0!09sCjMIGbeNM5-dpU(Rb8%^~p}8Gm4yx;* bVi5nULwOM2!+52|iRr0ePOUXpEf*I6$&}yK delta 325 zcmcbl{7zxRGRDb_Oj47dGfHuk7AK~s1|^nHW@j=p&CDxND9cPq%`dj*;!;omaSJl? zOOUuQ=J~PqPEKJ{oovshDg_f%k5#Z$D9Oky)`N*` zu43EHJoy`!`Q-gv<_LTLaLG*$;AUk8`B{Nmd9pv3(B>d+Z8kQD118TC6ocETt^*7O R1qF4mkzh`(HCHVc7XU|CYs>%u diff --git a/mobile/openapi/test/server_stats_response_dto_test.dart b/mobile/openapi/test/server_stats_response_dto_test.dart index 6e9fb783ad43f14312306352e205a21607e30cce..f5d2c3dc9b379d4b7cb171c8586ab959db4e60dd 100644 GIT binary patch delta 88 zcmcb~ah83<2S#B9jg-{1#L}D+g|ft)(o`!21I@`F7~^n=D^AX5GM6YVPE1e5rm=we FB>-kI9T%!-p$m~6rHR3>xw{G_bZ-+L79b2W=x6clQ%G_O#aMl3;?Ku B5aIv; diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index acb74ab44c..66c7d51108 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -37,6 +37,7 @@ describe('Album service', () => { shouldChangePassword: false, oauthId: '', tags: [], + assets: [], }); const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58'; const sharedAlbumOwnerId = '2222'; diff --git a/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts b/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts index 615acfcf05..ed7a071769 100644 --- a/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts @@ -2,28 +2,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { UsageByUserDto } from './usage-by-user-response.dto'; export class ServerStatsResponseDto { - constructor() { - this.photos = 0; - this.videos = 0; - this.usageByUser = []; - this.usageRaw = 0; - this.usage = ''; - } + @ApiProperty({ type: 'integer' }) + photos = 0; @ApiProperty({ type: 'integer' }) - photos!: number; - - @ApiProperty({ type: 'integer' }) - videos!: number; - - @ApiProperty({ type: 'integer' }) - objects!: number; + videos = 0; @ApiProperty({ type: 'integer', format: 'int64' }) - usageRaw!: number; - - @ApiProperty({ type: 'string' }) - usage!: string; + usage = 0; @ApiProperty({ isArray: true, @@ -37,5 +23,5 @@ export class ServerStatsResponseDto { }, ], }) - usageByUser!: UsageByUserDto[]; + usageByUser: UsageByUserDto[] = []; } diff --git a/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts b/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts index 7502d63afd..ac3a829077 100644 --- a/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts @@ -1,22 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; export class UsageByUserDto { - constructor(userId: string) { - this.userId = userId; - this.videos = 0; - this.photos = 0; - this.usageRaw = 0; - this.usage = '0B'; - } - @ApiProperty({ type: 'string' }) - userId: string; + userId!: string; + @ApiProperty({ type: 'string' }) + userFirstName!: string; + @ApiProperty({ type: 'string' }) + userLastName!: string; @ApiProperty({ type: 'integer' }) - videos: number; + photos!: number; @ApiProperty({ type: 'integer' }) - photos: number; + videos!: number; @ApiProperty({ type: 'integer', format: 'int64' }) - usageRaw!: number; - @ApiProperty({ type: 'string' }) - usage!: string; + usage!: number; } diff --git a/server/apps/immich/src/api-v1/server-info/server-info.module.ts b/server/apps/immich/src/api-v1/server-info/server-info.module.ts index 1b5154695e..25d8a19e22 100644 --- a/server/apps/immich/src/api-v1/server-info/server-info.module.ts +++ b/server/apps/immich/src/api-v1/server-info/server-info.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { ServerInfoService } from './server-info.service'; import { ServerInfoController } from './server-info.controller'; -import { AssetEntity } from '@app/infra'; +import { UserEntity } from '@app/infra'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ - imports: [TypeOrmModule.forFeature([AssetEntity])], + imports: [TypeOrmModule.forFeature([UserEntity])], controllers: [ServerInfoController], providers: [ServerInfoService], }) diff --git a/server/apps/immich/src/api-v1/server-info/server-info.service.ts b/server/apps/immich/src/api-v1/server-info/server-info.service.ts index 243d880dfe..779d4163e6 100644 --- a/server/apps/immich/src/api-v1/server-info/server-info.service.ts +++ b/server/apps/immich/src/api-v1/server-info/server-info.service.ts @@ -4,7 +4,7 @@ import { ServerInfoResponseDto } from './response-dto/server-info-response.dto'; import diskusage from 'diskusage'; import { ServerStatsResponseDto } from './response-dto/server-stats-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 { InjectRepository } from '@nestjs/typeorm'; import { asHumanReadable } from '../../utils/human-readable.util'; @@ -12,8 +12,8 @@ import { asHumanReadable } from '../../utils/human-readable.util'; @Injectable() export class ServerInfoService { constructor( - @InjectRepository(AssetEntity) - private assetRepository: Repository, + @InjectRepository(UserEntity) + private userRepository: Repository, ) {} async getServerInfo(): Promise { @@ -33,44 +33,48 @@ export class ServerInfoService { } async getStats(): Promise { - const serverStats = new ServerStatsResponseDto(); - type UserStatsQueryResponse = { - assetType: string; - assetCount: string; - totalSizeInBytes: string; - ownerId: string; + userId: string; + userFirstName: string; + userLastName: string; + photos: string; + videos: string; + usage: string; }; - const userStatsQueryResponse: UserStatsQueryResponse[] = await this.assetRepository - .createQueryBuilder('a') - .select('COUNT(a.id)', 'assetCount') - .addSelect('SUM(ei.fileSizeInByte)', 'totalSizeInBytes') - .addSelect('a."ownerId"') - .addSelect('a.type', 'assetType') - .where('a.isVisible = true') - .leftJoin('a.exifInfo', 'ei') - .groupBy('a."ownerId"') - .addGroupBy('a.type') + const userStatsQueryResponse: UserStatsQueryResponse[] = await this.userRepository + .createQueryBuilder('users') + .select('users.id', 'userId') + .addSelect('users.firstName', 'userFirstName') + .addSelect('users.lastName', 'userLastName') + .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') + .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos') + .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage') + .leftJoin('users.assets', 'assets') + .leftJoin('assets.exifInfo', 'exif') + .groupBy('users.id') + .orderBy('users.createdAt', 'ASC') .getRawMany(); - const tmpMap = new Map(); - const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id); - userStatsQueryResponse.forEach((r) => { - const usageByUser = getUsageByUser(r.ownerId); - usageByUser.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0; - usageByUser.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0; - usageByUser.usageRaw += parseInt(r.totalSizeInBytes); - usageByUser.usage = asHumanReadable(usageByUser.usageRaw); + const usageByUser = userStatsQueryResponse.map((userStats) => { + const usage = new UsageByUserDto(); + usage.userId = userStats.userId; + usage.userFirstName = userStats.userFirstName; + usage.userLastName = userStats.userLastName; + usage.photos = Number(userStats.photos); + usage.videos = Number(userStats.videos); + usage.usage = Number(userStats.usage); - serverStats.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0; - serverStats.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0; - serverStats.usageRaw += parseInt(r.totalSizeInBytes); - serverStats.usage = asHumanReadable(serverStats.usageRaw); - tmpMap.set(r.ownerId, usageByUser); + return usage; }); - 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; } diff --git a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts index 395a375738..877f60087d 100644 --- a/server/apps/immich/src/api-v1/tag/tag.service.spec.ts +++ b/server/apps/immich/src/api-v1/tag/tag.service.spec.ts @@ -25,6 +25,7 @@ describe('TagService', () => { deletedAt: undefined, updatedAt: '2022-12-02T19:29:23.603Z', tags: [], + assets: [], oauthId: 'oauth-id-1', }); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index a89e62a3ac..9661eba018 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -4883,25 +4883,29 @@ "userId": { "type": "string" }, - "videos": { - "type": "integer" + "userFirstName": { + "type": "string" + }, + "userLastName": { + "type": "string" }, "photos": { "type": "integer" }, - "usageRaw": { - "type": "integer", - "format": "int64" + "videos": { + "type": "integer" }, "usage": { - "type": "string" + "type": "integer", + "format": "int64" } }, "required": [ "userId", - "videos", + "userFirstName", + "userLastName", "photos", - "usageRaw", + "videos", "usage" ] }, @@ -4909,22 +4913,20 @@ "type": "object", "properties": { "photos": { - "type": "integer" + "type": "integer", + "default": 0 }, "videos": { - "type": "integer" - }, - "objects": { - "type": "integer" - }, - "usageRaw": { "type": "integer", - "format": "int64" + "default": 0 }, "usage": { - "type": "string" + "type": "integer", + "default": 0, + "format": "int64" }, "usageByUser": { + "default": [], "title": "Array of usage for each user", "example": [ { @@ -4942,8 +4944,6 @@ "required": [ "photos", "videos", - "objects", - "usageRaw", "usage", "usageByUser" ] diff --git a/server/libs/domain/src/user/user.service.spec.ts b/server/libs/domain/src/user/user.service.spec.ts index 938e324f5b..f1779899d8 100644 --- a/server/libs/domain/src/user/user.service.spec.ts +++ b/server/libs/domain/src/user/user.service.spec.ts @@ -33,6 +33,7 @@ const adminUser: UserEntity = Object.freeze({ createdAt: '2021-01-01', updatedAt: '2021-01-01', tags: [], + assets: [], }); const immichUser: UserEntity = Object.freeze({ @@ -48,6 +49,7 @@ const immichUser: UserEntity = Object.freeze({ createdAt: '2021-01-01', updatedAt: '2021-01-01', tags: [], + assets: [], }); const updatedImmichUser: UserEntity = Object.freeze({ @@ -63,6 +65,7 @@ const updatedImmichUser: UserEntity = Object.freeze({ createdAt: '2021-01-01', updatedAt: '2021-01-01', tags: [], + assets: [], }); const adminUserResponse = Object.freeze({ diff --git a/server/libs/domain/test/fixtures.ts b/server/libs/domain/test/fixtures.ts index 0d6ae37410..b8d63056a2 100644 --- a/server/libs/domain/test/fixtures.ts +++ b/server/libs/domain/test/fixtures.ts @@ -76,6 +76,7 @@ export const userEntityStub = { createdAt: '2021-01-01', updatedAt: '2021-01-01', tags: [], + assets: [], }), user1: Object.freeze({ ...authStub.user1, @@ -88,6 +89,7 @@ export const userEntityStub = { createdAt: '2021-01-01', updatedAt: '2021-01-01', tags: [], + assets: [], }), }; diff --git a/server/libs/infra/src/db/entities/user.entity.ts b/server/libs/infra/src/db/entities/user.entity.ts index d8724aab9a..5fca9a89d0 100644 --- a/server/libs/infra/src/db/entities/user.entity.ts +++ b/server/libs/infra/src/db/entities/user.entity.ts @@ -7,6 +7,7 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; +import { AssetEntity } from './asset.entity'; import { TagEntity } from './tag.entity'; @Entity('users') @@ -49,4 +50,7 @@ export class UserEntity { @OneToMany(() => TagEntity, (tag) => tag.user) tags!: TagEntity[]; + + @OneToMany(() => AssetEntity, (asset) => asset.owner) + assets!: AssetEntity[]; } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 45571fd946..1f8464367a 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1549,19 +1549,7 @@ export interface ServerStatsResponseDto { * @type {number} * @memberof ServerStatsResponseDto */ - 'objects': number; - /** - * - * @type {number} - * @memberof ServerStatsResponseDto - */ - 'usageRaw': number; - /** - * - * @type {string} - * @memberof ServerStatsResponseDto - */ - 'usage': string; + 'usage': number; /** * * @type {Array} @@ -2184,10 +2172,16 @@ export interface UsageByUserDto { 'userId': string; /** * - * @type {number} + * @type {string} * @memberof UsageByUserDto */ - 'videos': number; + 'userFirstName': string; + /** + * + * @type {string} + * @memberof UsageByUserDto + */ + 'userLastName': string; /** * * @type {number} @@ -2199,13 +2193,13 @@ export interface UsageByUserDto { * @type {number} * @memberof UsageByUserDto */ - 'usageRaw': number; + 'videos': number; /** * - * @type {string} + * @type {number} * @memberof UsageByUserDto */ - 'usage': string; + 'usage': number; } /** * diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index b20d897870..2243488c0d 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -1,43 +1,20 @@
@@ -45,14 +22,9 @@

TOTAL USAGE

- - - + + +
@@ -72,32 +44,19 @@ - {#if stats} - {#each stats.usageByUser as user, i} - - {getFullName(user.userId)} - {user.photos.toLocaleString($locale)} - {user.videos.toLocaleString($locale)} - {asByteUnitString(user.usageRaw)} - - {/each} - {:else} + {#each stats.usageByUser as user (user.userId)} - - - + {user.userFirstName} {user.userLastName} + {user.photos.toLocaleString($locale)} + {user.videos.toLocaleString($locale)} + {asByteUnitString(user.usage, $locale)} - {/if} + {/each} diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte index a3dcdab4b6..4a704a787d 100644 --- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte +++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte @@ -1,19 +1,14 @@ -{#if $page.data.allUsers} - -{/if} +