From 90ae908767985240281e751e03f55be84788dae5 Mon Sep 17 00:00:00 2001 From: David Requeno Date: Sat, 8 Nov 2025 00:13:13 -0600 Subject: [PATCH] Feature/fetch user data on demand in user detail dialog - Pass only userId when opening the user detail dialog from admin users - Load user details inside the dialog via GET /api/v1/admin/user/:id --- .../admin-users/admin-users.component.ts | 13 +- .../interfaces/interfaces.ts | 4 +- .../user-detail-dialog.component.ts | 39 +++++- .../user-detail-dialog.html | 112 +++++++++--------- apps/client/src/app/services/admin.service.ts | 6 + 5 files changed, 98 insertions(+), 76 deletions(-) diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 94b5839c6..f4fceccb9 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -1,4 +1,3 @@ -import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { getDateFnsLocale, @@ -56,6 +55,7 @@ import { NotificationService } from '../../core/notification/notification.servic import { AdminService } from '../../services/admin.service'; import { DataService } from '../../services/data.service'; import { ImpersonationStorageService } from '../../services/impersonation-storage.service'; +import { TokenStorageService } from '../../services/token-storage.service'; import { UserService } from '../../services/user/user.service'; import { UserDetailDialogParams } from '../user-detail-dialog/interfaces/interfaces'; import { GfUserDetailDialogComponent } from '../user-detail-dialog/user-detail-dialog.component'; @@ -283,22 +283,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { } private openUserDetailDialog(aUserId: string) { - const userData = this.dataSource.data.find(({ id }) => { - return id === aUserId; - }); - - if (!userData) { - this.router.navigate(['.'], { relativeTo: this.route }); - return; - } - const dialogRef = this.dialog.open< GfUserDetailDialogComponent, UserDetailDialogParams >(GfUserDetailDialogComponent, { autoFocus: false, data: { - userData, + userId: aUserId, deviceType: this.deviceType, hasPermissionForSubscription: this.hasPermissionForSubscription, locale: this.user?.settings?.locale diff --git a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts index d29bc01bc..b922e7a54 100644 --- a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts @@ -1,8 +1,6 @@ -import { AdminUsersResponse } from '@ghostfolio/common/interfaces'; - export interface UserDetailDialogParams { deviceType: string; hasPermissionForSubscription: boolean; locale: string; - userData: AdminUsersResponse['users'][0]; + userId: string; } diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts index bd336c4f8..ef3a8589d 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts @@ -1,5 +1,4 @@ -import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component'; -import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component'; +import { AdminUserResponse } from '@ghostfolio/common/interfaces'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule } from '@angular/common'; @@ -7,14 +6,20 @@ import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, + OnInit, Inject, OnDestroy } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; +import { Subject, EMPTY } from 'rxjs'; +import { catchError, finalize, takeUntil } from 'rxjs/operators'; +import { NotificationService } from '../../core/notification/notification.service'; +import { AdminService } from '../../services/admin.service'; +import { GfDialogFooterComponent } from '../dialog-footer/dialog-footer.component'; +import { GfDialogHeaderComponent } from '../dialog-header/dialog-header.component'; import { UserDetailDialogParams } from './interfaces/interfaces'; @Component({ @@ -33,14 +38,38 @@ import { UserDetailDialogParams } from './interfaces/interfaces'; styleUrls: ['./user-detail-dialog.component.scss'], templateUrl: './user-detail-dialog.html' }) -export class GfUserDetailDialogComponent implements OnDestroy { +export class GfUserDetailDialogComponent implements OnInit, OnDestroy { private unsubscribeSubject = new Subject(); + public isLoading = true; + public user: AdminUserResponse; public constructor( @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, - public dialogRef: MatDialogRef + public dialogRef: MatDialogRef, + private adminService: AdminService, + private notificationService: NotificationService ) {} + public ngOnInit(): void { + this.adminService + .fetchUserById(this.data.userId) + .pipe( + takeUntil(this.unsubscribeSubject), + finalize(() => (this.isLoading = false)), + catchError(() => { + this.notificationService.alert({ + title: $localize`User`, + message: $localize`Unable to load user` + }); + this.dialogRef.close(); + return EMPTY; + }) + ) + .subscribe((user) => { + this.user = user; + }); + } + public onClose() { this.dialogRef.close(); } diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html index 6bc468b59..588407cb4 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -6,74 +6,46 @@
-
-
- - User ID - -
-
- - Registration Date - -
-
- -
-
- - Role - -
- @if (data.hasPermissionForSubscription) { + @if (user) { +
- - Country + User ID +
+
+ + Registration Date
- } -
- -
-
- - Accounts -
-
- - Activities - + +
+
+ Role +
+ @if (data.hasPermissionForSubscription) { +
+ + Country + +
+ }
-
- @if (data.hasPermissionForSubscription) {
- Engagement per Day + Accounts
@@ -81,12 +53,38 @@ i18n size="medium" [locale]="data.locale" - [value]="data.userData.dailyApiRequests" + [value]="user.activityCount" > - API Requests Today + Activities
+ + @if (data.hasPermissionForSubscription) { +
+
+ + Engagement per Day + +
+
+ + API Requests Today + +
+
+ } }
diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 2f3040ba3..cd11a4c22 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -157,6 +157,12 @@ export class AdminService { return this.http.get('/api/v1/admin/user', { params }); } + public fetchUserById(id: string) { + return this.http.get< + import('@ghostfolio/common/interfaces').AdminUserResponse + >(`/api/v1/admin/user/${id}`); + } + public gather7Days() { return this.http.post('/api/v1/admin/gather', {}); }