diff --git a/mobile/openapi/doc/ServerStatsResponseDto.md b/mobile/openapi/doc/ServerStatsResponseDto.md index 3356c366f2..96446e1c28 100644 Binary files a/mobile/openapi/doc/ServerStatsResponseDto.md and b/mobile/openapi/doc/ServerStatsResponseDto.md differ diff --git a/mobile/openapi/doc/UsageByUserDto.md b/mobile/openapi/doc/UsageByUserDto.md index ffdc2a88ec..1d1bef8858 100644 Binary files a/mobile/openapi/doc/UsageByUserDto.md and b/mobile/openapi/doc/UsageByUserDto.md differ diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index 7dbc1db42a..aeb40c0c7f 100644 Binary files a/mobile/openapi/lib/model/server_stats_response_dto.dart and b/mobile/openapi/lib/model/server_stats_response_dto.dart differ diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index e18ac81de8..d2cbc4f41b 100644 Binary files a/mobile/openapi/lib/model/usage_by_user_dto.dart and b/mobile/openapi/lib/model/usage_by_user_dto.dart differ diff --git a/mobile/openapi/test/server_stats_response_dto_test.dart b/mobile/openapi/test/server_stats_response_dto_test.dart index 6e9fb783ad..f5d2c3dc9b 100644 Binary files a/mobile/openapi/test/server_stats_response_dto_test.dart and b/mobile/openapi/test/server_stats_response_dto_test.dart differ diff --git a/mobile/openapi/test/usage_by_user_dto_test.dart b/mobile/openapi/test/usage_by_user_dto_test.dart index 68efc27332..a4bec3f71d 100644 Binary files a/mobile/openapi/test/usage_by_user_dto_test.dart and b/mobile/openapi/test/usage_by_user_dto_test.dart differ 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} +