diff --git a/CHANGELOG.md b/CHANGELOG.md index f176dd4af..4437d077c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Refactored the get holding functionality in the portfolio service +- Changed the user data loading in the user detail dialog of the admin control panel’s users section to fetch data on demand - Improved the language localization for German (`de`) +- Upgraded `prisma` from version `6.18.0` to `6.19.0` ## 2.216.0 - 2025-11-10 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..6b3335927 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,12 @@ +import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces'; +import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component'; +import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; +import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { getDateFnsLocale, @@ -51,15 +59,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { ConfirmationDialogType } from '../../core/notification/confirmation-dialog/confirmation-dialog.type'; -import { NotificationService } from '../../core/notification/notification.service'; -import { AdminService } from '../../services/admin.service'; -import { DataService } from '../../services/data.service'; -import { ImpersonationStorageService } from '../../services/impersonation-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'; - @Component({ imports: [ CommonModule, @@ -283,25 +282,16 @@ 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, deviceType: this.deviceType, hasPermissionForSubscription: this.hasPermissionForSubscription, - locale: this.user?.settings?.locale + locale: this.user?.settings?.locale, + userId: aUserId }, height: this.deviceType === 'mobile' ? '98vh' : '60vh', width: this.deviceType === 'mobile' ? '100vw' : '50rem' 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..6dabf2f78 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,19 +1,24 @@ import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component'; import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { AdminUserResponse } from '@ghostfolio/common/interfaces'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, Inject, - OnDestroy + OnDestroy, + OnInit } 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 { EMPTY, Subject } from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; import { UserDetailDialogParams } from './interfaces/interfaces'; @@ -33,14 +38,36 @@ 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 OnDestroy, OnInit { + public user: AdminUserResponse; + private unsubscribeSubject = new Subject(); public constructor( + private adminService: AdminService, + private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, public dialogRef: MatDialogRef ) {} + public ngOnInit() { + this.adminService + .fetchUserById(this.data.userId) + .pipe( + takeUntil(this.unsubscribeSubject), + catchError(() => { + this.dialogRef.close(); + + return EMPTY; + }) + ) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); + } + 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..fcefee4f0 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 @@ -8,9 +8,7 @@
- - User ID - + User ID
Registration Date - Registration Date -
- - Role - + Role
@if (data.hasPermissionForSubscription) {
- - Country - + Country
}
@@ -46,20 +41,18 @@ i18n size="medium" [locale]="data.locale" - [value]="data.userData.accountCount" + [value]="user?.accountCount" + >Accounts - Accounts -
Activities - Activities -
@@ -71,20 +64,18 @@ size="medium" [locale]="data.locale" [precision]="0" - [value]="data.userData.engagement" + [value]="user?.engagement" + >Engagement per Day - Engagement per Day -
API Requests Today - API Requests Today -
} diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index fc4a88fa8..68a02facc 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -8,11 +8,12 @@ import { } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { - AssetProfileIdentifier, AdminData, AdminJobs, AdminMarketData, + AdminUserResponse, AdminUsersResponse, + AssetProfileIdentifier, DataProviderGhostfolioStatusResponse, DataProviderHistoricalResponse, EnhancedSymbolProfile, @@ -143,6 +144,10 @@ export class AdminService { return this.http.get('/api/v1/platform'); } + public fetchUserById(id: string) { + return this.http.get(`/api/v1/admin/user/${id}`); + } + public fetchUsers({ skip, take = DEFAULT_PAGE_SIZE diff --git a/package-lock.json b/package-lock.json index e44d8a513..a1d1551e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "@nestjs/schedule": "6.0.1", "@nestjs/serve-static": "5.0.4", "@openrouter/ai-sdk-provider": "0.7.2", - "@prisma/client": "6.18.0", + "@prisma/client": "6.19.0", "@simplewebauthn/browser": "13.1.0", "@simplewebauthn/server": "13.1.1", "@stripe/stripe-js": "7.9.0", @@ -146,7 +146,7 @@ "nx": "21.5.1", "prettier": "3.6.2", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "6.18.0", + "prisma": "6.19.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "8.3.0", @@ -11883,9 +11883,9 @@ "license": "MIT" }, "node_modules/@prisma/client": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", - "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz", + "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -11905,9 +11905,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz", - "integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz", + "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -11918,53 +11918,53 @@ } }, "node_modules/@prisma/debug": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", - "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz", + "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", - "integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz", + "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.18.0", - "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "@prisma/fetch-engine": "6.18.0", - "@prisma/get-platform": "6.18.0" + "@prisma/debug": "6.19.0", + "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", + "@prisma/fetch-engine": "6.19.0", + "@prisma/get-platform": "6.19.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz", - "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==", + "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz", + "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", - "integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz", + "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.18.0", - "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "@prisma/get-platform": "6.18.0" + "@prisma/debug": "6.19.0", + "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", + "@prisma/get-platform": "6.19.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz", - "integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz", + "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.18.0" + "@prisma/debug": "6.19.0" } }, "node_modules/@redis/client": { @@ -35617,15 +35617,15 @@ } }, "node_modules/prisma": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", - "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz", + "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.18.0", - "@prisma/engines": "6.18.0" + "@prisma/config": "6.19.0", + "@prisma/engines": "6.19.0" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index cc151f19c..5bf5e6775 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@nestjs/schedule": "6.0.1", "@nestjs/serve-static": "5.0.4", "@openrouter/ai-sdk-provider": "0.7.2", - "@prisma/client": "6.18.0", + "@prisma/client": "6.19.0", "@simplewebauthn/browser": "13.1.0", "@simplewebauthn/server": "13.1.1", "@stripe/stripe-js": "7.9.0", @@ -192,7 +192,7 @@ "nx": "21.5.1", "prettier": "3.6.2", "prettier-plugin-organize-attributes": "1.0.0", - "prisma": "6.18.0", + "prisma": "6.19.0", "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "8.3.0",