mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
feat: adding photo & video storage space to server stats (#14125)
* expose detailed user storage stats + display them in the storage per user table * chore: openapi & sql * fix: fix test stubs * fix: formatting errors, e2e test and server test * fix: upper lower case typo in spec file --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
24ae4ecff1
commit
f5c4af73aa
@ -163,11 +163,15 @@ describe('/server', () => {
|
|||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
photos: 0,
|
photos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
usageByUser: [
|
usageByUser: [
|
||||||
{
|
{
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
photos: 0,
|
photos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
userName: 'Immich Admin',
|
userName: 'Immich Admin',
|
||||||
userId: admin.userId,
|
userId: admin.userId,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
@ -176,6 +180,8 @@ describe('/server', () => {
|
|||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
photos: 0,
|
photos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
userName: 'User 1',
|
userName: 'User 1',
|
||||||
userId: nonAdmin.userId,
|
userId: nonAdmin.userId,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
|
@ -16,6 +16,8 @@ class ServerStatsResponseDto {
|
|||||||
this.photos = 0,
|
this.photos = 0,
|
||||||
this.usage = 0,
|
this.usage = 0,
|
||||||
this.usageByUser = const [],
|
this.usageByUser = const [],
|
||||||
|
this.usagePhotos = 0,
|
||||||
|
this.usageVideos = 0,
|
||||||
this.videos = 0,
|
this.videos = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,6 +27,10 @@ class ServerStatsResponseDto {
|
|||||||
|
|
||||||
List<UsageByUserDto> usageByUser;
|
List<UsageByUserDto> usageByUser;
|
||||||
|
|
||||||
|
int usagePhotos;
|
||||||
|
|
||||||
|
int usageVideos;
|
||||||
|
|
||||||
int videos;
|
int videos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -32,6 +38,8 @@ class ServerStatsResponseDto {
|
|||||||
other.photos == photos &&
|
other.photos == photos &&
|
||||||
other.usage == usage &&
|
other.usage == usage &&
|
||||||
_deepEquality.equals(other.usageByUser, usageByUser) &&
|
_deepEquality.equals(other.usageByUser, usageByUser) &&
|
||||||
|
other.usagePhotos == usagePhotos &&
|
||||||
|
other.usageVideos == usageVideos &&
|
||||||
other.videos == videos;
|
other.videos == videos;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,16 +48,20 @@ class ServerStatsResponseDto {
|
|||||||
(photos.hashCode) +
|
(photos.hashCode) +
|
||||||
(usage.hashCode) +
|
(usage.hashCode) +
|
||||||
(usageByUser.hashCode) +
|
(usageByUser.hashCode) +
|
||||||
|
(usagePhotos.hashCode) +
|
||||||
|
(usageVideos.hashCode) +
|
||||||
(videos.hashCode);
|
(videos.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ServerStatsResponseDto[photos=$photos, usage=$usage, usageByUser=$usageByUser, videos=$videos]';
|
String toString() => 'ServerStatsResponseDto[photos=$photos, usage=$usage, usageByUser=$usageByUser, usagePhotos=$usagePhotos, usageVideos=$usageVideos, videos=$videos]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'photos'] = this.photos;
|
json[r'photos'] = this.photos;
|
||||||
json[r'usage'] = this.usage;
|
json[r'usage'] = this.usage;
|
||||||
json[r'usageByUser'] = this.usageByUser;
|
json[r'usageByUser'] = this.usageByUser;
|
||||||
|
json[r'usagePhotos'] = this.usagePhotos;
|
||||||
|
json[r'usageVideos'] = this.usageVideos;
|
||||||
json[r'videos'] = this.videos;
|
json[r'videos'] = this.videos;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@ -66,6 +78,8 @@ class ServerStatsResponseDto {
|
|||||||
photos: mapValueOfType<int>(json, r'photos')!,
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
usage: mapValueOfType<int>(json, r'usage')!,
|
usage: mapValueOfType<int>(json, r'usage')!,
|
||||||
usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser']),
|
usageByUser: UsageByUserDto.listFromJson(json[r'usageByUser']),
|
||||||
|
usagePhotos: mapValueOfType<int>(json, r'usagePhotos')!,
|
||||||
|
usageVideos: mapValueOfType<int>(json, r'usageVideos')!,
|
||||||
videos: mapValueOfType<int>(json, r'videos')!,
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,6 +131,8 @@ class ServerStatsResponseDto {
|
|||||||
'photos',
|
'photos',
|
||||||
'usage',
|
'usage',
|
||||||
'usageByUser',
|
'usageByUser',
|
||||||
|
'usagePhotos',
|
||||||
|
'usageVideos',
|
||||||
'videos',
|
'videos',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
18
mobile/openapi/lib/model/usage_by_user_dto.dart
generated
18
mobile/openapi/lib/model/usage_by_user_dto.dart
generated
@ -16,6 +16,8 @@ class UsageByUserDto {
|
|||||||
required this.photos,
|
required this.photos,
|
||||||
required this.quotaSizeInBytes,
|
required this.quotaSizeInBytes,
|
||||||
required this.usage,
|
required this.usage,
|
||||||
|
required this.usagePhotos,
|
||||||
|
required this.usageVideos,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userName,
|
required this.userName,
|
||||||
required this.videos,
|
required this.videos,
|
||||||
@ -27,6 +29,10 @@ class UsageByUserDto {
|
|||||||
|
|
||||||
int usage;
|
int usage;
|
||||||
|
|
||||||
|
int usagePhotos;
|
||||||
|
|
||||||
|
int usageVideos;
|
||||||
|
|
||||||
String userId;
|
String userId;
|
||||||
|
|
||||||
String userName;
|
String userName;
|
||||||
@ -38,6 +44,8 @@ class UsageByUserDto {
|
|||||||
other.photos == photos &&
|
other.photos == photos &&
|
||||||
other.quotaSizeInBytes == quotaSizeInBytes &&
|
other.quotaSizeInBytes == quotaSizeInBytes &&
|
||||||
other.usage == usage &&
|
other.usage == usage &&
|
||||||
|
other.usagePhotos == usagePhotos &&
|
||||||
|
other.usageVideos == usageVideos &&
|
||||||
other.userId == userId &&
|
other.userId == userId &&
|
||||||
other.userName == userName &&
|
other.userName == userName &&
|
||||||
other.videos == videos;
|
other.videos == videos;
|
||||||
@ -48,12 +56,14 @@ class UsageByUserDto {
|
|||||||
(photos.hashCode) +
|
(photos.hashCode) +
|
||||||
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
|
||||||
(usage.hashCode) +
|
(usage.hashCode) +
|
||||||
|
(usagePhotos.hashCode) +
|
||||||
|
(usageVideos.hashCode) +
|
||||||
(userId.hashCode) +
|
(userId.hashCode) +
|
||||||
(userName.hashCode) +
|
(userName.hashCode) +
|
||||||
(videos.hashCode);
|
(videos.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UsageByUserDto[photos=$photos, quotaSizeInBytes=$quotaSizeInBytes, usage=$usage, userId=$userId, userName=$userName, videos=$videos]';
|
String toString() => 'UsageByUserDto[photos=$photos, quotaSizeInBytes=$quotaSizeInBytes, usage=$usage, usagePhotos=$usagePhotos, usageVideos=$usageVideos, userId=$userId, userName=$userName, videos=$videos]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -64,6 +74,8 @@ class UsageByUserDto {
|
|||||||
// json[r'quotaSizeInBytes'] = null;
|
// json[r'quotaSizeInBytes'] = null;
|
||||||
}
|
}
|
||||||
json[r'usage'] = this.usage;
|
json[r'usage'] = this.usage;
|
||||||
|
json[r'usagePhotos'] = this.usagePhotos;
|
||||||
|
json[r'usageVideos'] = this.usageVideos;
|
||||||
json[r'userId'] = this.userId;
|
json[r'userId'] = this.userId;
|
||||||
json[r'userName'] = this.userName;
|
json[r'userName'] = this.userName;
|
||||||
json[r'videos'] = this.videos;
|
json[r'videos'] = this.videos;
|
||||||
@ -82,6 +94,8 @@ class UsageByUserDto {
|
|||||||
photos: mapValueOfType<int>(json, r'photos')!,
|
photos: mapValueOfType<int>(json, r'photos')!,
|
||||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||||
usage: mapValueOfType<int>(json, r'usage')!,
|
usage: mapValueOfType<int>(json, r'usage')!,
|
||||||
|
usagePhotos: mapValueOfType<int>(json, r'usagePhotos')!,
|
||||||
|
usageVideos: mapValueOfType<int>(json, r'usageVideos')!,
|
||||||
userId: mapValueOfType<String>(json, r'userId')!,
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
userName: mapValueOfType<String>(json, r'userName')!,
|
userName: mapValueOfType<String>(json, r'userName')!,
|
||||||
videos: mapValueOfType<int>(json, r'videos')!,
|
videos: mapValueOfType<int>(json, r'videos')!,
|
||||||
@ -135,6 +149,8 @@ class UsageByUserDto {
|
|||||||
'photos',
|
'photos',
|
||||||
'quotaSizeInBytes',
|
'quotaSizeInBytes',
|
||||||
'usage',
|
'usage',
|
||||||
|
'usagePhotos',
|
||||||
|
'usageVideos',
|
||||||
'userId',
|
'userId',
|
||||||
'userName',
|
'userName',
|
||||||
'videos',
|
'videos',
|
||||||
|
@ -10966,7 +10966,9 @@
|
|||||||
{
|
{
|
||||||
"photos": 1,
|
"photos": 1,
|
||||||
"videos": 1,
|
"videos": 1,
|
||||||
"diskUsageRaw": 1
|
"diskUsageRaw": 2,
|
||||||
|
"usagePhotos": 1,
|
||||||
|
"usageVideos": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"items": {
|
"items": {
|
||||||
@ -10975,6 +10977,16 @@
|
|||||||
"title": "Array of usage for each user",
|
"title": "Array of usage for each user",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"usagePhotos": {
|
||||||
|
"default": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"usageVideos": {
|
||||||
|
"default": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"videos": {
|
"videos": {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@ -10984,6 +10996,8 @@
|
|||||||
"photos",
|
"photos",
|
||||||
"usage",
|
"usage",
|
||||||
"usageByUser",
|
"usageByUser",
|
||||||
|
"usagePhotos",
|
||||||
|
"usageVideos",
|
||||||
"videos"
|
"videos"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@ -12503,6 +12517,14 @@
|
|||||||
"format": "int64",
|
"format": "int64",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"usagePhotos": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"usageVideos": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -12517,6 +12539,8 @@
|
|||||||
"photos",
|
"photos",
|
||||||
"quotaSizeInBytes",
|
"quotaSizeInBytes",
|
||||||
"usage",
|
"usage",
|
||||||
|
"usagePhotos",
|
||||||
|
"usageVideos",
|
||||||
"userId",
|
"userId",
|
||||||
"userName",
|
"userName",
|
||||||
"videos"
|
"videos"
|
||||||
|
@ -969,6 +969,8 @@ export type UsageByUserDto = {
|
|||||||
photos: number;
|
photos: number;
|
||||||
quotaSizeInBytes: number | null;
|
quotaSizeInBytes: number | null;
|
||||||
usage: number;
|
usage: number;
|
||||||
|
usagePhotos: number;
|
||||||
|
usageVideos: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
videos: number;
|
videos: number;
|
||||||
@ -977,6 +979,8 @@ export type ServerStatsResponseDto = {
|
|||||||
photos: number;
|
photos: number;
|
||||||
usage: number;
|
usage: number;
|
||||||
usageByUser: UsageByUserDto[];
|
usageByUser: UsageByUserDto[];
|
||||||
|
usagePhotos: number;
|
||||||
|
usageVideos: number;
|
||||||
videos: number;
|
videos: number;
|
||||||
};
|
};
|
||||||
export type ServerStorageResponseDto = {
|
export type ServerStorageResponseDto = {
|
||||||
|
@ -86,6 +86,10 @@ export class UsageByUserDto {
|
|||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
usage!: number;
|
usage!: number;
|
||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usagePhotos!: number;
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usageVideos!: number;
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
quotaSizeInBytes!: number | null;
|
quotaSizeInBytes!: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +103,12 @@ export class ServerStatsResponseDto {
|
|||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
usage = 0;
|
usage = 0;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usagePhotos = 0;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
usageVideos = 0;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
isArray: true,
|
isArray: true,
|
||||||
type: UsageByUserDto,
|
type: UsageByUserDto,
|
||||||
@ -107,7 +117,9 @@ export class ServerStatsResponseDto {
|
|||||||
{
|
{
|
||||||
photos: 1,
|
photos: 1,
|
||||||
videos: 1,
|
videos: 1,
|
||||||
diskUsageRaw: 1,
|
diskUsageRaw: 2,
|
||||||
|
usagePhotos: 1,
|
||||||
|
usageVideos: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,8 @@ export interface UserStatsQueryResponse {
|
|||||||
photos: number;
|
photos: number;
|
||||||
videos: number;
|
videos: number;
|
||||||
usage: number;
|
usage: number;
|
||||||
|
usagePhotos: number;
|
||||||
|
usageVideos: number;
|
||||||
quotaSizeInBytes: number | null;
|
quotaSizeInBytes: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,23 @@ SELECT
|
|||||||
"assets"."libraryId" IS NULL
|
"assets"."libraryId" IS NULL
|
||||||
),
|
),
|
||||||
0
|
0
|
||||||
) AS "usage"
|
) AS "usage",
|
||||||
|
COALESCE(
|
||||||
|
SUM("exif"."fileSizeInByte") FILTER (
|
||||||
|
WHERE
|
||||||
|
"assets"."libraryId" IS NULL
|
||||||
|
AND "assets"."type" = 'IMAGE'
|
||||||
|
),
|
||||||
|
0
|
||||||
|
) AS "usagePhotos",
|
||||||
|
COALESCE(
|
||||||
|
SUM("exif"."fileSizeInByte") FILTER (
|
||||||
|
WHERE
|
||||||
|
"assets"."libraryId" IS NULL
|
||||||
|
AND "assets"."type" = 'VIDEO'
|
||||||
|
),
|
||||||
|
0
|
||||||
|
) AS "usageVideos"
|
||||||
FROM
|
FROM
|
||||||
"users" "users"
|
"users" "users"
|
||||||
LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"
|
LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"
|
||||||
|
@ -108,6 +108,14 @@ export class UserRepository implements IUserRepository {
|
|||||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
|
.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(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
|
||||||
.addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage')
|
.addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage')
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'IMAGE'), 0)`,
|
||||||
|
'usagePhotos',
|
||||||
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'VIDEO'), 0)`,
|
||||||
|
'usageVideos',
|
||||||
|
)
|
||||||
.addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
|
.addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
|
||||||
.leftJoin('users.assets', 'assets')
|
.leftJoin('users.assets', 'assets')
|
||||||
.leftJoin('assets.exifInfo', 'exif')
|
.leftJoin('assets.exifInfo', 'exif')
|
||||||
@ -119,6 +127,8 @@ export class UserRepository implements IUserRepository {
|
|||||||
stat.photos = Number(stat.photos);
|
stat.photos = Number(stat.photos);
|
||||||
stat.videos = Number(stat.videos);
|
stat.videos = Number(stat.videos);
|
||||||
stat.usage = Number(stat.usage);
|
stat.usage = Number(stat.usage);
|
||||||
|
stat.usagePhotos = Number(stat.usagePhotos);
|
||||||
|
stat.usageVideos = Number(stat.usageVideos);
|
||||||
stat.quotaSizeInBytes = stat.quotaSizeInBytes;
|
stat.quotaSizeInBytes = stat.quotaSizeInBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +185,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 10,
|
photos: 10,
|
||||||
videos: 11,
|
videos: 11,
|
||||||
usage: 12_345,
|
usage: 12_345,
|
||||||
|
usagePhotos: 1,
|
||||||
|
usageVideos: 11_345,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -193,6 +195,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 10,
|
photos: 10,
|
||||||
videos: 20,
|
videos: 20,
|
||||||
usage: 123_456,
|
usage: 123_456,
|
||||||
|
usagePhotos: 100,
|
||||||
|
usageVideos: 23_456,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -201,6 +205,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 100,
|
photos: 100,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
usage: 987_654,
|
usage: 987_654,
|
||||||
|
usagePhotos: 900,
|
||||||
|
usageVideos: 87_654,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -209,11 +215,15 @@ describe(ServerService.name, () => {
|
|||||||
photos: 120,
|
photos: 120,
|
||||||
videos: 31,
|
videos: 31,
|
||||||
usage: 1_123_455,
|
usage: 1_123_455,
|
||||||
|
usagePhotos: 1001,
|
||||||
|
usageVideos: 122_455,
|
||||||
usageByUser: [
|
usageByUser: [
|
||||||
{
|
{
|
||||||
photos: 10,
|
photos: 10,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
usage: 12_345,
|
usage: 12_345,
|
||||||
|
usagePhotos: 1,
|
||||||
|
usageVideos: 11_345,
|
||||||
userName: '1 User',
|
userName: '1 User',
|
||||||
userId: 'user1',
|
userId: 'user1',
|
||||||
videos: 11,
|
videos: 11,
|
||||||
@ -222,6 +232,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 10,
|
photos: 10,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
usage: 123_456,
|
usage: 123_456,
|
||||||
|
usagePhotos: 100,
|
||||||
|
usageVideos: 23_456,
|
||||||
userName: '2 User',
|
userName: '2 User',
|
||||||
userId: 'user2',
|
userId: 'user2',
|
||||||
videos: 20,
|
videos: 20,
|
||||||
@ -230,6 +242,8 @@ describe(ServerService.name, () => {
|
|||||||
photos: 100,
|
photos: 100,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
usage: 987_654,
|
usage: 987_654,
|
||||||
|
usagePhotos: 900,
|
||||||
|
usageVideos: 87_654,
|
||||||
userName: '3 User',
|
userName: '3 User',
|
||||||
userId: 'user3',
|
userId: 'user3',
|
||||||
videos: 0,
|
videos: 0,
|
||||||
|
@ -126,11 +126,16 @@ export class ServerService extends BaseService {
|
|||||||
usage.photos = user.photos;
|
usage.photos = user.photos;
|
||||||
usage.videos = user.videos;
|
usage.videos = user.videos;
|
||||||
usage.usage = user.usage;
|
usage.usage = user.usage;
|
||||||
|
usage.usagePhotos = user.usagePhotos;
|
||||||
|
usage.usageVideos = user.usageVideos;
|
||||||
usage.quotaSizeInBytes = user.quotaSizeInBytes;
|
usage.quotaSizeInBytes = user.quotaSizeInBytes;
|
||||||
|
|
||||||
serverStats.photos += usage.photos;
|
serverStats.photos += usage.photos;
|
||||||
serverStats.videos += usage.videos;
|
serverStats.videos += usage.videos;
|
||||||
serverStats.usage += usage.usage;
|
serverStats.usage += usage.usage;
|
||||||
|
serverStats.usagePhotos += usage.usagePhotos;
|
||||||
|
serverStats.usageVideos += usage.usageVideos;
|
||||||
|
|
||||||
serverStats.usageByUser.push(usage);
|
serverStats.usageByUser.push(usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
photos: 0,
|
photos: 0,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
usage: 0,
|
usage: 0,
|
||||||
|
usagePhotos: 0,
|
||||||
|
usageVideos: 0,
|
||||||
usageByUser: [],
|
usageByUser: [],
|
||||||
},
|
},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@ -105,8 +107,12 @@
|
|||||||
class="flex h-[50px] w-full place-items-center text-center odd:bg-immich-gray even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50"
|
class="flex h-[50px] w-full place-items-center text-center odd:bg-immich-gray even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50"
|
||||||
>
|
>
|
||||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
|
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
|
||||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.photos.toLocaleString($locale)}</td>
|
<td class="w-1/4 text-ellipsis px-2 text-sm"
|
||||||
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td>
|
>{user.photos.toLocaleString($locale)} ({getByteUnitString(user.usagePhotos, $locale, 0)})</td
|
||||||
|
>
|
||||||
|
<td class="w-1/4 text-ellipsis px-2 text-sm"
|
||||||
|
>{user.videos.toLocaleString($locale)} ({getByteUnitString(user.usageVideos, $locale, 0)})</td
|
||||||
|
>
|
||||||
<td class="w-1/4 text-ellipsis px-2 text-sm">
|
<td class="w-1/4 text-ellipsis px-2 text-sm">
|
||||||
{getByteUnitString(user.usage, $locale, 0)}
|
{getByteUnitString(user.usage, $locale, 0)}
|
||||||
{#if user.quotaSizeInBytes}
|
{#if user.quotaSizeInBytes}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user