From 41a127e2ab8d5c3bd130eeb8e342a8668582fe0b Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 12 May 2025 16:56:36 +0200 Subject: [PATCH 1/8] refactor: avatar selector modal (#18228) --- .../navigation-bar/account-info-panel.svelte | 32 ++----------- .../navigation-bar/avatar-selector.svelte | 28 ----------- web/src/lib/modals/AvatarEditModal.svelte | 47 +++++++++++++++++++ 3 files changed, 50 insertions(+), 57 deletions(-) delete mode 100644 web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte create mode 100644 web/src/lib/modals/AvatarEditModal.svelte diff --git a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte index 5b778cf227..c16003ca66 100644 --- a/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/account-info-panel.svelte @@ -5,15 +5,13 @@ import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import { AppRoute } from '$lib/constants'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; + import AvatarEditModal from '$lib/modals/AvatarEditModal.svelte'; import { user } from '$lib/stores/user.store'; - import { handleError } from '$lib/utils/handle-error'; - import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk'; import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; - import { NotificationType, notificationController } from '../notification/notification'; import UserAvatar from '../user-avatar.svelte'; - import AvatarSelector from './avatar-selector.svelte'; interface Props { onLogout: () => void; @@ -21,26 +19,6 @@ } let { onLogout, onClose = () => {} }: Props = $props(); - - let isShowSelectAvatar = $state(false); - - const handleSaveProfile = async (color: UserAvatarColor) => { - try { - if ($user.profileImagePath !== '') { - await deleteProfileImage(); - } - - $user = await updateMyUser({ userUpdateMeDto: { avatarColor: color } }); - isShowSelectAvatar = false; - - notificationController.show({ - message: $t('saved_profile'), - type: NotificationType.Info, - }); - } catch (error) { - handleError(error, $t('errors.unable_to_save_profile')); - } - };
(isShowSelectAvatar = true)} + onclick={() => modalManager.show(AvatarEditModal, {})} />
@@ -111,7 +89,3 @@ > - -{#if isShowSelectAvatar} - (isShowSelectAvatar = false)} onChoose={handleSaveProfile} /> -{/if} diff --git a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte deleted file mode 100644 index ce441d553b..0000000000 --- a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - -
-
- {#each colors as color (color)} - - {/each} -
-
-
diff --git a/web/src/lib/modals/AvatarEditModal.svelte b/web/src/lib/modals/AvatarEditModal.svelte new file mode 100644 index 0000000000..2bfee599af --- /dev/null +++ b/web/src/lib/modals/AvatarEditModal.svelte @@ -0,0 +1,47 @@ + + + + +
+ {#each colors as color (color)} + + {/each} +
+
+
From eb8dfa283e061c8dc5f8c7e2d01df1a0420d42f0 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 12 May 2025 14:15:15 -0400 Subject: [PATCH 2/8] fix(web): no rounded map on /map page (#18232) --- .../(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5875309b05..b1fff3a0cd 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -72,7 +72,7 @@ {#if $featureFlags.loaded && $featureFlags.map}
- +
From 3066c8198cb23092bb0cb15af1b6702e7c2c8ab2 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 12 May 2025 16:50:26 -0400 Subject: [PATCH 3/8] feat(web): user detail page (#18230) feat: user detail page --- i18n/en.json | 7 + mobile/openapi/README.md | 1 + mobile/openapi/lib/api/users_admin_api.dart | 83 ++++- open-api/immich-openapi-specs.json | 75 ++++ open-api/typescript-sdk/src/fetch-client.ts | 41 ++- .../src/controllers/user-admin.controller.ts | 11 + server/src/dtos/user.dto.ts | 5 +- server/src/repositories/user.repository.ts | 4 +- server/src/services/user-admin.service.ts | 11 +- .../dialog/confirm-dialog.svelte | 2 +- .../navigation-bar/account-info-panel.svelte | 2 +- .../side-bar/admin-side-bar.svelte | 2 +- web/src/lib/constants.ts | 2 +- web/src/lib/modals/UserEditModal.svelte | 118 +----- web/src/routes/admin/+page.ts | 2 +- web/src/routes/admin/user-management/+page.ts | 19 +- .../{user-management => users}/+page.svelte | 23 +- web/src/routes/admin/users/+page.ts | 18 + web/src/routes/admin/users/[id]/+page.svelte | 343 ++++++++++++++++++ web/src/routes/admin/users/[id]/+page.ts | 31 ++ 20 files changed, 640 insertions(+), 160 deletions(-) rename web/src/routes/admin/{user-management => users}/+page.svelte (91%) create mode 100644 web/src/routes/admin/users/+page.ts create mode 100644 web/src/routes/admin/users/[id]/+page.svelte create mode 100644 web/src/routes/admin/users/[id]/+page.ts diff --git a/i18n/en.json b/i18n/en.json index 2db9976fa6..fde78a34a3 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -362,6 +362,7 @@ "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.", "user_delete_immediately": "{user}'s account and assets will be queued for permanent deletion immediately.", "user_delete_immediately_checkbox": "Queue user and assets for immediate deletion", + "user_details": "User Details", "user_management": "User Management", "user_password_has_been_reset": "The user's password has been reset:", "user_password_reset_description": "Please provide the temporary password to the user and inform them they will need to change the password at their next login.", @@ -1290,6 +1291,7 @@ "notification_permission_list_tile_enable_button": "Enable Notifications", "notification_permission_list_tile_title": "Notification Permission", "notification_toggle_setting_description": "Enable email notifications", + "email_notifications": "Email notifications", "notifications": "Notifications", "notifications_setting_description": "Manage notifications", "oauth": "OAuth", @@ -1394,6 +1396,7 @@ "previous_or_next_photo": "Previous or next photo", "primary": "Primary", "privacy": "Privacy", + "profile": "Profile", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", @@ -1753,6 +1756,7 @@ "storage": "Storage space", "storage_label": "Storage label", "storage_usage": "{used} of {available} used", + "storage_quota": "Storage Quota", "submit": "Submit", "suggestions": "Suggestions", "sunrise_on_the_beach": "Sunrise on the beach", @@ -1857,6 +1861,7 @@ "upload_success": "Upload success, refresh the page to see new upload assets.", "upload_to_immich": "Upload to Immich ({count})", "uploading": "Uploading", + "id": "ID", "url": "URL", "usage": "Usage", "use_current_connection": "use current connection", @@ -1864,6 +1869,8 @@ "user": "User", "user_id": "User ID", "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "created_at": "Created", + "updated_at": "Updated", "user_purchase_settings": "Purchase", "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a141d465d1..9a3055911d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -253,6 +253,7 @@ Class | Method | HTTP request | Description *UsersAdminApi* | [**deleteUserAdmin**](doc//UsersAdminApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} | *UsersAdminApi* | [**getUserAdmin**](doc//UsersAdminApi.md#getuseradmin) | **GET** /admin/users/{id} | *UsersAdminApi* | [**getUserPreferencesAdmin**](doc//UsersAdminApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences | +*UsersAdminApi* | [**getUserStatisticsAdmin**](doc//UsersAdminApi.md#getuserstatisticsadmin) | **GET** /admin/users/{id}/statistics | *UsersAdminApi* | [**restoreUserAdmin**](doc//UsersAdminApi.md#restoreuseradmin) | **POST** /admin/users/{id}/restore | *UsersAdminApi* | [**searchUsersAdmin**](doc//UsersAdminApi.md#searchusersadmin) | **GET** /admin/users | *UsersAdminApi* | [**updateUserAdmin**](doc//UsersAdminApi.md#updateuseradmin) | **PUT** /admin/users/{id} | diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index b4508d7dcd..58263504ce 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -211,6 +211,76 @@ class UsersAdminApi { return null; } + /// Performs an HTTP 'GET /admin/users/{id}/statistics' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [bool] isFavorite: + /// + /// * [bool] isTrashed: + /// + /// * [AssetVisibility] visibility: + Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/users/{id}/statistics' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isTrashed != null) { + queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); + } + if (visibility != null) { + queryParams.addAll(_queryParams('', 'visibility', visibility)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [bool] isFavorite: + /// + /// * [bool] isTrashed: + /// + /// * [AssetVisibility] visibility: + Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { + final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetStatsResponseDto',) as AssetStatsResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /admin/users/{id}/restore' operation and returns the [Response]. /// Parameters: /// @@ -262,8 +332,10 @@ class UsersAdminApi { /// Performs an HTTP 'GET /admin/users' operation and returns the [Response]. /// Parameters: /// + /// * [String] id: + /// /// * [bool] withDeleted: - Future searchUsersAdminWithHttpInfo({ bool? withDeleted, }) async { + Future searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users'; @@ -274,6 +346,9 @@ class UsersAdminApi { final headerParams = {}; final formParams = {}; + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } if (withDeleted != null) { queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); } @@ -294,9 +369,11 @@ class UsersAdminApi { /// Parameters: /// + /// * [String] id: + /// /// * [bool] withDeleted: - Future?> searchUsersAdmin({ bool? withDeleted, }) async { - final response = await searchUsersAdminWithHttpInfo( withDeleted: withDeleted, ); + Future?> searchUsersAdmin({ String? id, bool? withDeleted, }) async { + final response = await searchUsersAdminWithHttpInfo( id: id, withDeleted: withDeleted, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a98750edaa..3c0dc09953 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -345,6 +345,15 @@ "get": { "operationId": "searchUsersAdmin", "parameters": [ + { + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "withDeleted", "required": false, @@ -701,6 +710,72 @@ ] } }, + "/admin/users/{id}/statistics": { + "get": { + "operationId": "getUserStatisticsAdmin", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isTrashed", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "visibility", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetVisibility" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetStatsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users (admin)" + ] + } + }, "/albums": { "get": { "operationId": "getAllAlbums", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 41898e12da..144e7f8ac1 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -224,6 +224,11 @@ export type UserPreferencesUpdateDto = { sharedLinks?: SharedLinksUpdate; tags?: TagsUpdate; }; +export type AssetStatsResponseDto = { + images: number; + total: number; + videos: number; +}; export type AlbumUserResponseDto = { role: AlbumUserRole; user: UserResponseDto; @@ -462,11 +467,6 @@ export type AssetJobsDto = { assetIds: string[]; name: AssetJobName; }; -export type AssetStatsResponseDto = { - images: number; - total: number; - videos: number; -}; export type UpdateAssetDto = { dateTimeOriginal?: string; description?: string; @@ -1502,13 +1502,15 @@ export function sendTestEmailAdmin({ systemConfigSmtpDto }: { body: systemConfigSmtpDto }))); } -export function searchUsersAdmin({ withDeleted }: { +export function searchUsersAdmin({ id, withDeleted }: { + id?: string; withDeleted?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: UserAdminResponseDto[]; }>(`/admin/users${QS.query(QS.explode({ + id, withDeleted }))}`, { ...opts @@ -1596,6 +1598,23 @@ export function restoreUserAdmin({ id }: { method: "POST" })); } +export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility }: { + id: string; + isFavorite?: boolean; + isTrashed?: boolean; + visibility?: AssetVisibility; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetStatsResponseDto; + }>(`/admin/users/${encodeURIComponent(id)}/statistics${QS.query(QS.explode({ + isFavorite, + isTrashed, + visibility + }))}`, { + ...opts + })); +} export function getAllAlbums({ assetId, shared }: { assetId?: string; shared?: boolean; @@ -3552,6 +3571,11 @@ export enum UserStatus { Removing = "removing", Deleted = "deleted" } +export enum AssetVisibility { + Archive = "archive", + Timeline = "timeline", + Hidden = "hidden" +} export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" @@ -3661,11 +3685,6 @@ export enum Permission { AdminUserUpdate = "admin.user.update", AdminUserDelete = "admin.user.delete" } -export enum AssetVisibility { - Archive = "archive", - Timeline = "timeline", - Hidden = "hidden" -} export enum AssetMediaStatus { Created = "created", Replaced = "replaced", diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index 4dfeae949a..83d7caef08 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { @@ -57,6 +58,16 @@ export class UserAdminController { return this.service.delete(auth, id, dto); } + @Get(':id/statistics') + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) + getUserStatisticsAdmin( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Query() dto: AssetStatsDto, + ): Promise { + return this.service.getStatistics(auth, id, dto); + } + @Get(':id/preferences') @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 9efb531bc7..9d43e53f89 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -4,7 +4,7 @@ import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from import { User, UserAdmin } from 'src/database'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; +import { Optional, PinCode, ValidateBoolean, ValidateUUID, toEmail, toSanitized } from 'src/validation'; export class UserUpdateMeDto { @Optional() @@ -67,6 +67,9 @@ export const mapUser = (entity: User | UserAdmin): UserResponseDto => { export class UserAdminSearchDto { @ValidateBoolean({ optional: true }) withDeleted?: boolean; + + @ValidateUUID({ optional: true }) + id?: string; } export class UserAdminCreateDto { diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index f8710746aa..6972479df6 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -14,6 +14,7 @@ import { asUuid } from 'src/utils/database'; type Upsert = Insertable; export interface UserListFilter { + id?: string; withDeleted?: boolean; } @@ -141,12 +142,13 @@ export class UserRepository { { name: 'with deleted', params: [{ withDeleted: true }] }, { name: 'without deleted', params: [{ withDeleted: false }] }, ) - getList({ withDeleted }: UserListFilter = {}) { + getList({ id, withDeleted }: UserListFilter = {}) { return this.db .selectFrom('users') .select(columns.userAdmin) .select(withMetadata) .$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null)) + .$if(!!id, (eb) => eb.where('users.id', '=', id!)) .orderBy('createdAt', 'desc') .execute(); } diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 38c0106f4b..d1fe5ce67e 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { SALT_ROUNDS } from 'src/constants'; +import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { @@ -18,7 +19,10 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti @Injectable() export class UserAdminService extends BaseService { async search(auth: AuthDto, dto: UserAdminSearchDto): Promise { - const users = await this.userRepository.getList({ withDeleted: dto.withDeleted }); + const users = await this.userRepository.getList({ + id: dto.id, + withDeleted: dto.withDeleted, + }); return users.map((user) => mapUserAdmin(user)); } @@ -109,6 +113,11 @@ export class UserAdminService extends BaseService { return mapUserAdmin(user); } + async getStatistics(auth: AuthDto, id: string, dto: AssetStatsDto): Promise { + const stats = await this.assetRepository.getStatistics(auth.user.id, dto); + return mapStats(stats); + } + async getPreferences(auth: AuthDto, id: string): Promise { await this.findOrFail(id, { withDeleted: true }); const metadata = await this.userRepository.getMetadata(id); diff --git a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte index 3d6582d65f..75c07aebc6 100644 --- a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte +++ b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte @@ -44,7 +44,7 @@ -
+
{#if !hideCancelButton} {#if $user.isAdmin} - {/if} - - -
- -
- -
+
+ +
diff --git a/web/src/routes/admin/+page.ts b/web/src/routes/admin/+page.ts index 0d53c4ef2b..bab3c1ea66 100644 --- a/web/src/routes/admin/+page.ts +++ b/web/src/routes/admin/+page.ts @@ -3,5 +3,5 @@ import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; export const load = (() => { - redirect(302, AppRoute.ADMIN_USER_MANAGEMENT); + redirect(302, AppRoute.ADMIN_USERS); }) satisfies PageLoad; diff --git a/web/src/routes/admin/user-management/+page.ts b/web/src/routes/admin/user-management/+page.ts index 0a6af40c69..6ff068a1fb 100644 --- a/web/src/routes/admin/user-management/+page.ts +++ b/web/src/routes/admin/user-management/+page.ts @@ -1,18 +1,5 @@ -import { authenticate, requestServerInfo } from '$lib/utils/auth'; -import { getFormatter } from '$lib/utils/i18n'; -import { searchUsersAdmin } from '@immich/sdk'; +import { AppRoute } from '$lib/constants'; +import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async () => { - await authenticate({ admin: true }); - await requestServerInfo(); - const allUsers = await searchUsersAdmin({ withDeleted: true }); - const $t = await getFormatter(); - - return { - allUsers, - meta: { - title: $t('admin.user_management'), - }, - }; -}) satisfies PageLoad; +export const load = (() => redirect(307, AppRoute.ADMIN_USERS)) satisfies PageLoad; diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/users/+page.svelte similarity index 91% rename from web/src/routes/admin/user-management/+page.svelte rename to web/src/routes/admin/users/+page.svelte index 5b6246be8c..75b35491f6 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/users/+page.svelte @@ -6,7 +6,7 @@ NotificationType, notificationController, } from '$lib/components/shared-components/notification/notification'; - import PasswordResetSuccess from '$lib/forms/password-reset-success.svelte'; + import { AppRoute } from '$lib/constants'; import { modalManager } from '$lib/managers/modal-manager.svelte'; import UserCreateModal from '$lib/modals/UserCreateModal.svelte'; import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte'; @@ -18,7 +18,7 @@ import { websocketEvents } from '$lib/stores/websocket'; import { getByteUnitString } from '$lib/utils/byte-units'; import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; - import { Button, IconButton } from '@immich/ui'; + import { Button, IconButton, Link } from '@immich/ui'; import { mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import { DateTime } from 'luxon'; import { onMount } from 'svelte'; @@ -64,20 +64,9 @@ }; const handleEdit = async (dto: UserAdminResponseDto) => { - const result = await modalManager.show(UserEditModal, { user: dto, canResetPassword: dto.id !== $user.id }); - switch (result?.action) { - case 'resetPassword': { - await modalManager.show(PasswordResetSuccess, { newPassword: result.data }); - break; - } - case 'update': { - await refresh(); - break; - } - case 'resetPinCode': { - notificationController.show({ type: NotificationType.Info, message: $t('pin_code_reset_successfully') }); - break; - } + const result = await modalManager.show(UserEditModal, { user: dto }); + if (result) { + await refresh(); } }; @@ -123,7 +112,7 @@ : 'bg-immich-bg dark:bg-immich-dark-gray/50'}" > {immichUser.email}{immichUser.email} {immichUser.name} diff --git a/web/src/routes/admin/users/+page.ts b/web/src/routes/admin/users/+page.ts new file mode 100644 index 0000000000..0a6af40c69 --- /dev/null +++ b/web/src/routes/admin/users/+page.ts @@ -0,0 +1,18 @@ +import { authenticate, requestServerInfo } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { searchUsersAdmin } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async () => { + await authenticate({ admin: true }); + await requestServerInfo(); + const allUsers = await searchUsersAdmin({ withDeleted: true }); + const $t = await getFormatter(); + + return { + allUsers, + meta: { + title: $t('admin.user_management'), + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte new file mode 100644 index 0000000000..b0a9327fcc --- /dev/null +++ b/web/src/routes/admin/users/[id]/+page.svelte @@ -0,0 +1,343 @@ + + + + {#snippet buttons()} + + {#if canResetPassword} + + {/if} + + + + + + {/snippet} +
+ +
+
+ + {user.name} +
+
+
+ + + +
+
+
+ + +
+ + {$t('profile')} +
+
+ + +
+ {$t('name')} + {user.name} +
+
+ {$t('email')} + {user.email} +
+
+ {$t('created_at')} + {user.createdAt} +
+
+ {$t('updated_at')} + {user.updatedAt} +
+
+ {$t('id')} + {user.id} +
+
+
+
+
+ + +
+ + {$t('features')} +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + {$t('storage_quota')} +
+
+ +
+ {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0} + + {$t('storage_usage', { + values: { + used: getByteUnitString(usedBytes, $locale, 3), + available: getByteUnitString(availableBytes, $locale, 3), + }, + })} + + {:else} + + + {$t('unlimited')} + + {/if} +
+ + {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0} +
+

{$t('storage')}

+
+
+
+
+ {/if} +
+
+
+
+
+
diff --git a/web/src/routes/admin/users/[id]/+page.ts b/web/src/routes/admin/users/[id]/+page.ts new file mode 100644 index 0000000000..ddf3ddbef7 --- /dev/null +++ b/web/src/routes/admin/users/[id]/+page.ts @@ -0,0 +1,31 @@ +import { AppRoute } from '$lib/constants'; +import { authenticate, requestServerInfo } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getUserPreferencesAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk'; +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params }) => { + await authenticate({ admin: true }); + await requestServerInfo(); + const [user] = await searchUsersAdmin({ id: params.id }).catch(() => []); + if (!user) { + redirect(302, AppRoute.ADMIN_USERS); + } + + const [userPreferences, userStatistics] = await Promise.all([ + getUserPreferencesAdmin({ id: user.id }), + getUserStatisticsAdmin({ id: user.id }), + ]); + + const $t = await getFormatter(); + + return { + user, + userPreferences, + userStatistics, + meta: { + title: $t('admin.user_details'), + }, + }; +}) satisfies PageLoad; From 7544a678ec2a68df6163a9c5213c8e749b27a14a Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 12 May 2025 23:17:01 +0200 Subject: [PATCH 4/8] refactor: remove unnecessary bg-color attributes and move to ui lib vars (#18234) --- .../server-stats/server-stats-panel.svelte | 4 +--- .../components/album-page/album-title.svelte | 6 ++--- .../components/album-page/album-viewer.svelte | 24 +++++++++---------- .../album-page/albums-table-row.svelte | 2 +- .../asset-viewer/asset-viewer.svelte | 6 ++--- .../asset-viewer/download-panel.svelte | 2 +- .../faces-page/assign-face-side-panel.svelte | 20 ++++++++-------- .../manage-people-visibility.svelte | 4 ++-- .../faces-page/merge-face-selector.svelte | 8 +++---- .../faces-page/people-search.svelte | 2 +- .../faces-page/person-side-panel.svelte | 20 ++++++++-------- .../faces-page/unmerge-face-selector.svelte | 6 ++--- .../forms/library-import-paths-form.svelte | 8 ++----- .../forms/library-scan-settings-form.svelte | 8 ++----- .../lib/components/layouts/ErrorLayout.svelte | 6 ++--- .../layouts/user-page-layout.svelte | 2 +- .../onboarding-page/onboarding-theme.svelte | 4 ++-- .../photos-page/asset-date-group.svelte | 9 ++++--- .../components/photos-page/skeleton.svelte | 2 +- .../individual-shared-viewer.svelte | 2 +- .../album-selection-modal.svelte | 16 ++++++------- .../shared-components/control-app-bar.svelte | 4 ++-- .../navigation-bar/navigation-bar.svelte | 2 +- .../scrubber/scrubber.svelte | 6 ++--- .../side-bar/side-bar-section.svelte | 2 +- .../upload-asset-preview.svelte | 6 +---- .../user-api-key-list.svelte | 6 ++--- .../user-usage-statistic.svelte | 8 +++---- .../[[assetId=id]]/+page.svelte | 8 +++---- .../[[assetId=id]]/+page.svelte | 2 +- web/src/routes/(user)/people/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 5 +--- .../[[assetId=id]]/+page.svelte | 18 +++++++------- .../admin/library-management/+page.svelte | 4 +--- web/src/routes/admin/users/+page.svelte | 6 ++--- 36 files changed, 106 insertions(+), 136 deletions(-) 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 3dcd3b4594..2f8d391954 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 @@ -103,9 +103,7 @@ class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg" > {#each stats.usageByUser as user (user.userId)} - + {user.userName} {user.photos.toLocaleString($locale)} ({getByteUnitString(user.usagePhotos, $locale, 0)}) - import { updateAlbumInfo } from '@immich/sdk'; - import { handleError } from '$lib/utils/handle-error'; import { shortcut } from '$lib/actions/shortcut'; + import { handleError } from '$lib/utils/handle-error'; + import { updateAlbumInfo } from '@immich/sdk'; import { t } from 'svelte-i18n'; interface Props { @@ -40,7 +40,7 @@ onblur={handleUpdateName} class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' - : 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray" + : 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray" type="text" bind:value={newAlbumName} disabled={!isOwned} diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 1f15e22d9e..f3688e780c 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -1,11 +1,18 @@ goto(`${AppRoute.ALBUMS}/${album.id}`)} {oncontextmenu} > diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 5606da31a9..1e3c6135f0 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -529,7 +529,7 @@
($isShowDetail = false)} /> @@ -540,7 +540,7 @@
@@ -589,7 +589,7 @@

{$t('downloading').toUpperCase()}

diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index 19a99fdb94..c1906b70e0 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -1,20 +1,20 @@
-
+
@@ -33,7 +33,7 @@
diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index d15b371049..747dda91a6 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -51,7 +51,7 @@