From d096caccacfe1d6c31b329b0ce49742537b49452 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 15 Jan 2024 09:04:29 -0600 Subject: [PATCH] chore(web): quota enhancement (#6371) * chore(web): quota enhancement * show quota in user table * update quota for single user ioption * Add a note how to set unlimited storage * fixed deletion doesn't update quota * refactor relation * fixed test * re-refactor * update sql * fix e2e test * Update server/src/domain/user/user.service.ts Co-authored-by: Jason Rasmussen * revert e2e test --------- Co-authored-by: Jason Rasmussen --- server/src/domain/asset/asset.service.spec.ts | 96 +++++++++++-- server/src/domain/asset/asset.service.ts | 18 ++- .../domain/repositories/user.repository.ts | 2 +- server/src/domain/user/user.service.ts | 7 +- .../infra/repositories/asset.repository.ts | 10 -- .../src/infra/repositories/user.repository.ts | 14 +- server/src/infra/sql/asset.repository.sql | 135 +++++------------- server/src/infra/sql/user.repository.sql | 16 +++ .../components/forms/edit-user-form.svelte | 1 + .../routes/admin/user-management/+page.svelte | 12 ++ 10 files changed, 183 insertions(+), 128 deletions(-) diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index cfc39480c9..3bc39379ba 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -845,7 +845,16 @@ describe(AssetService.name, () => { it('should remove faces', async () => { const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] }; - when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace); + when(assetMock.getById) + .calledWith(assetWithFace.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetWithFace); await sut.handleAssetDeletion({ id: assetWithFace.id }); @@ -870,7 +879,16 @@ describe(AssetService.name, () => { }); it('should update stack parent if asset has stack children', async () => { - when(assetMock.getById).calledWith(assetStub.primaryImage.id).mockResolvedValue(assetStub.primaryImage); + when(assetMock.getById) + .calledWith(assetStub.primaryImage.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetStub.primaryImage); await sut.handleAssetDeletion({ id: assetStub.primaryImage.id }); @@ -883,7 +901,16 @@ describe(AssetService.name, () => { }); it('should not schedule delete-files job for readonly assets', async () => { - when(assetMock.getById).calledWith(assetStub.readOnly.id).mockResolvedValue(assetStub.readOnly); + when(assetMock.getById) + .calledWith(assetStub.readOnly.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetStub.readOnly); await sut.handleAssetDeletion({ id: assetStub.readOnly.id }); @@ -903,7 +930,16 @@ describe(AssetService.name, () => { }); it('should process assets from external library with fromExternal flag', async () => { - when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external); + when(assetMock.getById) + .calledWith(assetStub.external.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetStub.external); await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true }); @@ -926,6 +962,27 @@ describe(AssetService.name, () => { }); it('should delete a live photo', async () => { + when(assetMock.getById) + .calledWith(assetStub.livePhotoStillAsset.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetStub.livePhotoStillAsset); + when(assetMock.getById) + .calledWith(assetStub.livePhotoMotionAsset.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetStub.livePhotoMotionAsset); + await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id }); expect(jobMock.queue.mock.calls).toEqual([ @@ -950,7 +1007,16 @@ describe(AssetService.name, () => { }); it('should update usage', async () => { - when(assetMock.getById).calledWith(assetStub.image.id).mockResolvedValue(assetStub.image); + when(assetMock.getById) + .calledWith(assetStub.image.id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }) + .mockResolvedValue(assetStub.image); await sut.handleAssetDeletion({ id: assetStub.image.id }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000); @@ -1005,7 +1071,13 @@ describe(AssetService.name, () => { accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); when(assetMock.getById) - .calledWith(assetStub.image.id) + .calledWith(assetStub.image.id, { + faces: { + person: true, + }, + library: true, + stack: true, + }) .mockResolvedValue(assetStub.image as AssetEntity); await sut.updateStackParent(authStub.user1, { @@ -1032,7 +1104,13 @@ describe(AssetService.name, () => { accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id])); accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); when(assetMock.getById) - .calledWith(assetStub.primaryImage.id) + .calledWith(assetStub.primaryImage.id, { + faces: { + person: true, + }, + library: true, + stack: true, + }) .mockResolvedValue(assetStub.primaryImage as AssetEntity); await sut.updateStackParent(authStub.user1, { @@ -1042,7 +1120,9 @@ describe(AssetService.name, () => { expect(assetMock.updateAll).toBeCalledWith( [assetStub.primaryImage.id, 'stack-child-asset-1', 'stack-child-asset-2'], - { stackParentId: 'new' }, + { + stackParentId: 'new', + }, ); }); }); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 9eaca4cbe3..dd0a0b39a6 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -465,7 +465,15 @@ export class AssetService { async handleAssetDeletion(job: IAssetDeletionJob) { const { id, fromExternal } = job; - const asset = await this.assetRepository.getById(id); + const asset = await this.assetRepository.getById(id, { + faces: { + person: true, + }, + library: true, + stack: true, + exifInfo: true, + }); + if (!asset) { return false; } @@ -554,7 +562,13 @@ export class AssetService { await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId); const childIds: string[] = []; - const oldParent = await this.assetRepository.getById(oldParentId); + const oldParent = await this.assetRepository.getById(oldParentId, { + faces: { + person: true, + }, + library: true, + stack: true, + }); if (oldParent != null) { childIds.push(oldParent.id); // Get all children of old parent diff --git a/server/src/domain/repositories/user.repository.ts b/server/src/domain/repositories/user.repository.ts index 26e2511481..cecdb0b06e 100644 --- a/server/src/domain/repositories/user.repository.ts +++ b/server/src/domain/repositories/user.repository.ts @@ -34,5 +34,5 @@ export interface IUserRepository { delete(user: UserEntity, hard?: boolean): Promise; restore(user: UserEntity): Promise; updateUsage(id: string, delta: number): Promise; - syncUsage(): Promise; + syncUsage(id?: string): Promise; } diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 136b9c7cfd..bdb8f74ed7 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -60,7 +60,12 @@ export class UserService { } async update(auth: AuthDto, dto: UpdateUserDto): Promise { - await this.findOrFail(dto.id, {}); + const user = await this.findOrFail(dto.id, {}); + + if (dto.quotaSizeInBytes && user.quotaSizeInBytes !== dto.quotaSizeInBytes) { + await this.userRepository.syncUsage(dto.id); + } + return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser); } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index f734cb8ba3..6e18048716 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -363,16 +363,6 @@ export class AssetRepository implements IAssetRepository { @GenerateSql({ params: [DummyValue.UUID] }) getById(id: string, relations: FindOptionsRelations): Promise { - if (!relations) { - relations = { - faces: { - person: true, - }, - library: true, - stack: true, - }; - } - return this.repository.findOne({ where: { id }, relations, diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/infra/repositories/user.repository.ts index 639d335987..4990893014 100644 --- a/server/src/infra/repositories/user.repository.ts +++ b/server/src/infra/repositories/user.repository.ts @@ -115,7 +115,8 @@ export class UserRepository implements IUserRepository { await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta); } - async syncUsage() { + @GenerateSql({ params: [DummyValue.UUID] }) + async syncUsage(id?: string) { const subQuery = this.assetRepository .createQueryBuilder('assets') .select('COALESCE(SUM(exif."fileSizeInByte"), 0)') @@ -123,12 +124,17 @@ export class UserRepository implements IUserRepository { .where('assets.ownerId = users.id') .withDeleted(); - await this.userRepository + const query = this.userRepository .createQueryBuilder('users') .leftJoin('users.assets', 'assets') .update() - .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }) - .execute(); + .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }); + + if (id) { + query.where('users.id = :id', { id }); + } + + await query.execute(); } private async save(user: Partial) { diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql index 8dd2cf729d..f36ec07645 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/infra/sql/asset.repository.sql @@ -397,109 +397,40 @@ WHERE ) -- AssetRepository.getById -SELECT DISTINCT - "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId" FROM - ( - SELECT - "AssetEntity"."id" AS "AssetEntity_id", - "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", - "AssetEntity"."ownerId" AS "AssetEntity_ownerId", - "AssetEntity"."libraryId" AS "AssetEntity_libraryId", - "AssetEntity"."deviceId" AS "AssetEntity_deviceId", - "AssetEntity"."type" AS "AssetEntity_type", - "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", - "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", - "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", - "AssetEntity"."createdAt" AS "AssetEntity_createdAt", - "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", - "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", - "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", - "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", - "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", - "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", - "AssetEntity"."isArchived" AS "AssetEntity_isArchived", - "AssetEntity"."isExternal" AS "AssetEntity_isExternal", - "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", - "AssetEntity"."isOffline" AS "AssetEntity_isOffline", - "AssetEntity"."checksum" AS "AssetEntity_checksum", - "AssetEntity"."duration" AS "AssetEntity_duration", - "AssetEntity"."isVisible" AS "AssetEntity_isVisible", - "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", - "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", - "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", - "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId", - "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", - "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", - "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", - "AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth", - "AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight", - "AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1", - "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", - "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", - "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId", - "8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden", - "AssetEntity__AssetEntity_library"."id" AS "AssetEntity__AssetEntity_library_id", - "AssetEntity__AssetEntity_library"."name" AS "AssetEntity__AssetEntity_library_name", - "AssetEntity__AssetEntity_library"."ownerId" AS "AssetEntity__AssetEntity_library_ownerId", - "AssetEntity__AssetEntity_library"."type" AS "AssetEntity__AssetEntity_library_type", - "AssetEntity__AssetEntity_library"."importPaths" AS "AssetEntity__AssetEntity_library_importPaths", - "AssetEntity__AssetEntity_library"."exclusionPatterns" AS "AssetEntity__AssetEntity_library_exclusionPatterns", - "AssetEntity__AssetEntity_library"."createdAt" AS "AssetEntity__AssetEntity_library_createdAt", - "AssetEntity__AssetEntity_library"."updatedAt" AS "AssetEntity__AssetEntity_library_updatedAt", - "AssetEntity__AssetEntity_library"."deletedAt" AS "AssetEntity__AssetEntity_library_deletedAt", - "AssetEntity__AssetEntity_library"."refreshedAt" AS "AssetEntity__AssetEntity_library_refreshedAt", - "AssetEntity__AssetEntity_library"."isVisible" AS "AssetEntity__AssetEntity_library_isVisible", - "AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id", - "AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId", - "AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId", - "AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId", - "AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId", - "AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type", - "AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath", - "AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath", - "AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath", - "AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash", - "AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath", - "AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt", - "AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt", - "AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt", - "AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt", - "AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime", - "AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt", - "AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite", - "AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived", - "AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal", - "AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly", - "AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline", - "AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum", - "AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration", - "AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible", - "AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId", - "AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName", - "AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath", - "AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId" - FROM - "assets" "AssetEntity" - LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id" - LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" - LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" - LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id" - WHERE - ("AssetEntity"."id" = $1) - ) "distinctAlias" -ORDER BY - "AssetEntity_id" ASC + "assets" "AssetEntity" +WHERE + ("AssetEntity"."id" = $1) LIMIT 1 diff --git a/server/src/infra/sql/user.repository.sql b/server/src/infra/sql/user.repository.sql index 7440eef917..f773f27835 100644 --- a/server/src/infra/sql/user.repository.sql +++ b/server/src/infra/sql/user.repository.sql @@ -150,3 +150,19 @@ GROUP BY "users"."id" ORDER BY "users"."createdAt" ASC + +-- UserRepository.syncUsage +UPDATE "users" +SET + "quotaUsageInBytes" = ( + SELECT + COALESCE(SUM(exif."fileSizeInByte"), 0) + FROM + "assets" "assets" + LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" + WHERE + "assets"."ownerId" = users.id + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE + users.id = $1 diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index bb8d487dca..ba1c4a1b91 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -104,6 +104,7 @@
+

Note: Enter 0 for unlimited quota

diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index f430da1e0a..baf771ec22 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -14,6 +14,7 @@ import type { PageData } from './$types'; import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { user } from '$lib/stores/user.store'; + import { asByteUnitString } from '$lib/utils/byte-units'; export let data: PageData; @@ -171,6 +172,7 @@ Email Name + Has quota Can import Action @@ -191,6 +193,15 @@ >{immichUser.email} {immichUser.name} + +
+ {#if immichUser.quotaSizeInBytes && immichUser.quotaSizeInBytes > 0} + {asByteUnitString(immichUser.quotaSizeInBytes, $locale)} + {:else} + + {/if} +
+
{#if immichUser.externalPath} @@ -200,6 +211,7 @@ {/if}
+ {#if !isDeleted(immichUser)}