Browse Source

Merge remote-tracking branch 'origin/main' into task/enforce-module-boundaries-for-api-common

pull/5925/head
KenTandrian 1 month ago
parent
commit
6ac719f9a8
  1. 2
      CHANGELOG.md
  2. 30
      apps/client/src/app/components/admin-users/admin-users.component.ts
  3. 4
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  4. 33
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  5. 39
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
  6. 7
      apps/client/src/app/services/admin.service.ts
  7. 72
      package-lock.json
  8. 4
      package.json

2
CHANGELOG.md

@ -10,7 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Refactored the get holding functionality in the portfolio service - 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`) - Improved the language localization for German (`de`)
- Upgraded `prisma` from version `6.18.0` to `6.19.0`
## 2.216.0 - 2025-11-10 ## 2.216.0 - 2025-11-10

30
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 { 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 { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { import {
getDateFnsLocale, getDateFnsLocale,
@ -51,15 +59,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; 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({ @Component({
imports: [ imports: [
CommonModule, CommonModule,
@ -283,25 +282,16 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
} }
private openUserDetailDialog(aUserId: string) { 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< const dialogRef = this.dialog.open<
GfUserDetailDialogComponent, GfUserDetailDialogComponent,
UserDetailDialogParams UserDetailDialogParams
>(GfUserDetailDialogComponent, { >(GfUserDetailDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
userData,
deviceType: this.deviceType, deviceType: this.deviceType,
hasPermissionForSubscription: this.hasPermissionForSubscription, hasPermissionForSubscription: this.hasPermissionForSubscription,
locale: this.user?.settings?.locale locale: this.user?.settings?.locale,
userId: aUserId
}, },
height: this.deviceType === 'mobile' ? '98vh' : '60vh', height: this.deviceType === 'mobile' ? '98vh' : '60vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'

4
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -1,8 +1,6 @@
import { AdminUsersResponse } from '@ghostfolio/common/interfaces';
export interface UserDetailDialogParams { export interface UserDetailDialogParams {
deviceType: string; deviceType: string;
hasPermissionForSubscription: boolean; hasPermissionForSubscription: boolean;
locale: string; locale: string;
userData: AdminUsersResponse['users'][0]; userId: string;
} }

33
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 { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.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 { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
Inject, Inject,
OnDestroy OnDestroy,
OnInit
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } 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'; import { UserDetailDialogParams } from './interfaces/interfaces';
@ -33,14 +38,36 @@ import { UserDetailDialogParams } from './interfaces/interfaces';
styleUrls: ['./user-detail-dialog.component.scss'], styleUrls: ['./user-detail-dialog.component.scss'],
templateUrl: './user-detail-dialog.html' templateUrl: './user-detail-dialog.html'
}) })
export class GfUserDetailDialogComponent implements OnDestroy { export class GfUserDetailDialogComponent implements OnDestroy, OnInit {
public user: AdminUserResponse;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams,
public dialogRef: MatDialogRef<GfUserDetailDialogComponent> public dialogRef: MatDialogRef<GfUserDetailDialogComponent>
) {} ) {}
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() { public onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }

39
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -8,9 +8,7 @@
<div class="container p-0"> <div class="container p-0">
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="data.userData.id"> <gf-value i18n size="medium" [value]="user?.id">User ID</gf-value>
User ID
</gf-value>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
@ -18,24 +16,21 @@
size="medium" size="medium"
[isDate]="true" [isDate]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="data.userData.createdAt" [value]="user?.createdAt"
>Registration Date</gf-value
> >
Registration Date
</gf-value>
</div> </div>
</div> </div>
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="data.userData.role"> <gf-value i18n size="medium" [value]="user?.role">Role</gf-value>
Role
</gf-value>
</div> </div>
@if (data.hasPermissionForSubscription) { @if (data.hasPermissionForSubscription) {
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="data.userData.country"> <gf-value i18n size="medium" [value]="user?.country"
Country >Country</gf-value
</gf-value> >
</div> </div>
} }
</div> </div>
@ -46,20 +41,18 @@
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="data.userData.accountCount" [value]="user?.accountCount"
>Accounts</gf-value
> >
Accounts
</gf-value>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="data.userData.activityCount" [value]="user?.activityCount"
>Activities</gf-value
> >
Activities
</gf-value>
</div> </div>
</div> </div>
@ -71,20 +64,18 @@
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[precision]="0" [precision]="0"
[value]="data.userData.engagement" [value]="user?.engagement"
>Engagement per Day</gf-value
> >
Engagement per Day
</gf-value>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
i18n i18n
size="medium" size="medium"
[locale]="data.locale" [locale]="data.locale"
[value]="data.userData.dailyApiRequests" [value]="user?.dailyApiRequests"
>API Requests Today</gf-value
> >
API Requests Today
</gf-value>
</div> </div>
</div> </div>
} }

7
apps/client/src/app/services/admin.service.ts

@ -8,11 +8,12 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { import {
AssetProfileIdentifier,
AdminData, AdminData,
AdminJobs, AdminJobs,
AdminMarketData, AdminMarketData,
AdminUserResponse,
AdminUsersResponse, AdminUsersResponse,
AssetProfileIdentifier,
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
@ -143,6 +144,10 @@ export class AdminService {
return this.http.get<Platform[]>('/api/v1/platform'); return this.http.get<Platform[]>('/api/v1/platform');
} }
public fetchUserById(id: string) {
return this.http.get<AdminUserResponse>(`/api/v1/admin/user/${id}`);
}
public fetchUsers({ public fetchUsers({
skip, skip,
take = DEFAULT_PAGE_SIZE take = DEFAULT_PAGE_SIZE

72
package-lock.json

@ -38,7 +38,7 @@
"@nestjs/schedule": "6.0.1", "@nestjs/schedule": "6.0.1",
"@nestjs/serve-static": "5.0.4", "@nestjs/serve-static": "5.0.4",
"@openrouter/ai-sdk-provider": "0.7.2", "@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.18.0", "@prisma/client": "6.19.0",
"@simplewebauthn/browser": "13.1.0", "@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1", "@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0", "@stripe/stripe-js": "7.9.0",
@ -146,7 +146,7 @@
"nx": "21.5.1", "nx": "21.5.1",
"prettier": "3.6.2", "prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.18.0", "prisma": "6.19.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "8.3.0", "replace-in-file": "8.3.0",
@ -11883,9 +11883,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz",
"integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@ -11905,9 +11905,9 @@
} }
}, },
"node_modules/@prisma/config": { "node_modules/@prisma/config": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz",
"integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==", "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -11918,53 +11918,53 @@
} }
}, },
"node_modules/@prisma/debug": { "node_modules/@prisma/debug": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz",
"integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==", "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.18.0", "@prisma/debug": "6.19.0",
"@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
"@prisma/fetch-engine": "6.18.0", "@prisma/fetch-engine": "6.19.0",
"@prisma/get-platform": "6.18.0" "@prisma/get-platform": "6.19.0"
} }
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz",
"integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==", "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz",
"integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==", "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.18.0", "@prisma/debug": "6.19.0",
"@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
"@prisma/get-platform": "6.18.0" "@prisma/get-platform": "6.19.0"
} }
}, },
"node_modules/@prisma/get-platform": { "node_modules/@prisma/get-platform": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz",
"integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==", "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/debug": "6.18.0" "@prisma/debug": "6.19.0"
} }
}, },
"node_modules/@redis/client": { "node_modules/@redis/client": {
@ -35617,15 +35617,15 @@
} }
}, },
"node_modules/prisma": { "node_modules/prisma": {
"version": "6.18.0", "version": "6.19.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz",
"integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@prisma/config": "6.18.0", "@prisma/config": "6.19.0",
"@prisma/engines": "6.18.0" "@prisma/engines": "6.19.0"
}, },
"bin": { "bin": {
"prisma": "build/index.js" "prisma": "build/index.js"

4
package.json

@ -84,7 +84,7 @@
"@nestjs/schedule": "6.0.1", "@nestjs/schedule": "6.0.1",
"@nestjs/serve-static": "5.0.4", "@nestjs/serve-static": "5.0.4",
"@openrouter/ai-sdk-provider": "0.7.2", "@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/client": "6.18.0", "@prisma/client": "6.19.0",
"@simplewebauthn/browser": "13.1.0", "@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1", "@simplewebauthn/server": "13.1.1",
"@stripe/stripe-js": "7.9.0", "@stripe/stripe-js": "7.9.0",
@ -192,7 +192,7 @@
"nx": "21.5.1", "nx": "21.5.1",
"prettier": "3.6.2", "prettier": "3.6.2",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "6.18.0", "prisma": "6.19.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "8.3.0", "replace-in-file": "8.3.0",

Loading…
Cancel
Save