Browse Source

Merge branch 'main' into bugfix/portfolio-snapshot-cache-write-and-initialization-retry-limit

pull/7517/head
Thomas Kaul 4 weeks ago
committed by GitHub
parent
commit
86566be488
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  3. 2
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  4. 12
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  5. 10
      apps/api/src/app/user/user.controller.ts
  6. 4
      apps/api/src/app/user/user.module.ts
  7. 2
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  8. 59
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  9. 2
      apps/client/src/app/components/admin-users/admin-users.html
  10. 7
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  11. 28
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed the loading state in the user detail dialog of the admin control panel’s users section
- Fixed a race condition where the portfolio snapshot computation was completed before its result had been cached, causing a redundant recomputation
- Fixed an endless loop in the portfolio snapshot computation if the computed result could not be read from the cache

2
apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts

@ -142,6 +142,8 @@ export class AssetProfilesController {
});
return this.assetProfilesService.createSplit({
dataSource,
symbol,
symbolProfileId,
date: parseISO(data.date),
denominator: data.denominator,

2
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -8,6 +8,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { DataGatheringQueueModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
@ -23,6 +24,7 @@ import { AssetProfilesService } from './asset-profiles.service';
ApiModule,
AssetProfileSplitModule,
BenchmarkModule,
DataGatheringQueueModule,
DataProviderModule,
ExchangeRateDataModule,
MarketDataModule,

12
apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts

@ -5,6 +5,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
import {
@ -33,6 +34,7 @@ export class AssetProfilesService {
private readonly activitiesService: ActivitiesService,
private readonly assetProfileSplitService: AssetProfileSplitService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
@ -41,22 +43,28 @@ export class AssetProfilesService {
) {}
public async createSplit({
dataSource,
date,
denominator,
numerator,
symbol,
symbolProfileId
}: {
date: Date;
denominator: number;
numerator: number;
symbolProfileId: string;
}) {
return this.assetProfileSplitService.upsert({
} & AssetProfileIdentifier) {
const assetProfileSplit = await this.assetProfileSplitService.upsert({
date,
denominator,
numerator,
symbolProfileId
});
await this.dataGatheringService.gatherSymbol({ dataSource, symbol });
return assetProfileSplit;
}
public async deleteSplit({

10
apps/api/src/app/user/user.controller.ts

@ -1,7 +1,9 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { CustomThrottlerGuard } from '@ghostfolio/api/guards/custom-throttler.guard';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { decodeDataSource } from '@ghostfolio/api/helper/data-source.helper';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
@ -119,6 +121,7 @@ export class UserController {
@Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getUser(
@Headers('accept-language') acceptLanguage: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
@ -165,6 +168,7 @@ export class UserController {
@Put('setting')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
if (
size(data) === 1 &&
@ -192,6 +196,12 @@ export class UserController {
data
);
if (userSettings['filters.dataSource']) {
userSettings['filters.dataSource'] = decodeDataSource(
userSettings['filters.dataSource']
);
}
for (const key in userSettings) {
if (userSettings[key] === false || userSettings[key] === null) {
delete userSettings[key];

4
apps/api/src/app/user/user.module.ts

@ -1,6 +1,7 @@
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
@ -30,7 +31,8 @@ import { UserService } from './user.service';
PropertyModule,
RedactValuesInResponseModule,
SubscriptionModule,
TagModule
TagModule,
TransformDataSourceInResponseModule
],
providers: [UserService]
})

2
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -78,6 +78,7 @@ export class TransformDataSourceInResponseInterceptor<
valueMap,
object: data,
paths: [
'["filters.dataSource"]',
'activities[*].assetProfile.dataSource',
'activities[*].dataSource',
'assetProfile.dataSource',
@ -88,6 +89,7 @@ export class TransformDataSourceInResponseInterceptor<
'holdings[*].assetProfile.dataSource',
'holdings[*].dataSource',
'items[*].dataSource',
'settings["filters.dataSource"]',
'watchlist[*].dataSource'
]
});

59
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -514,10 +514,6 @@
<div class="container mt-3 p-0">
<div class="no-gutters row w-100">
<div class="col-12">
<p class="text-muted" i18n>
Splits are stored for this asset profile, but are not applied
to the portfolio calculation yet.
</p>
@if (splits.length > 0) {
<table class="gf-table mb-3 w-100">
<thead>
@ -580,38 +576,33 @@
</mat-datepicker-toggle>
<mat-datepicker #dateOfSplit />
</mat-form-field>
<mat-form-field appearance="outline" class="mb-3 w-100">
<mat-label i18n>Shares After</mat-label>
<input
formControlName="numerator"
matInput
step="1"
type="number"
/>
<mat-hint
><ng-container i18n>Numerator</ng-container>,
<ng-container i18n
>e.g. 4 for a 4:1 split or 1 for a 1:10 reverse
split</ng-container
></mat-hint
<div class="align-items-center d-flex mb-3">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
</mat-form-field>
<mat-form-field appearance="outline" class="mb-3 w-100">
<mat-label i18n>Shares Before</mat-label>
<input
formControlName="denominator"
matInput
step="1"
type="number"
/>
<mat-hint
><ng-container i18n>Denominator</ng-container>,
<ng-container i18n
>e.g. 1 for a 4:1 split or 10 for a 1:10 reverse
split</ng-container
></mat-hint
<mat-label i18n>Shares After</mat-label>
<input
formControlName="numerator"
matInput
step="1"
type="number"
/>
</mat-form-field>
<div class="mx-2">:</div>
<mat-form-field
appearance="outline"
class="w-100 without-hint"
>
</mat-form-field>
<mat-label i18n>Shares Before</mat-label>
<input
formControlName="denominator"
matInput
step="1"
type="number"
/>
</mat-form-field>
</div>
<div class="text-right">
<button
color="primary"

2
apps/client/src/app/components/admin-users/admin-users.html

@ -251,7 +251,7 @@
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete User</span>
<span i18n>Delete</span>
</span>
</button>
</mat-menu>

7
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -56,14 +56,18 @@ export class GfUserDetailDialogComponent implements OnInit {
protected readonly baseCurrency: string;
protected readonly canDeleteUser = canDeleteUser;
protected readonly getCountryName = getCountryName;
protected isLoading = true;
protected readonly subscriptionsDataSource =
new MatTableDataSource<Subscription>();
protected readonly subscriptionsDisplayedColumns = [
'createdAt',
'type',
'price',
'expiresAt'
];
protected user: AdminUserResponse;
protected readonly data = inject<UserDetailDialogParams>(MAT_DIALOG_DATA);
@ -72,6 +76,7 @@ export class GfUserDetailDialogComponent implements OnInit {
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly dialogRef =
inject<MatDialogRef<GfUserDetailDialogComponent, UserDetailDialogResult>>(
MatDialogRef
@ -101,6 +106,8 @@ export class GfUserDetailDialogComponent implements OnInit {
this.subscriptionsDataSource.data = this.user.subscriptions ?? [];
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}

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

@ -36,12 +36,14 @@
i18n
size="medium"
[enableCopyToClipboardButton]="true"
[value]="user?.id"
[value]="isLoading ? undefined : user.id"
>User ID</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="user?.role">Role</gf-value>
<gf-value i18n size="medium" [value]="isLoading ? undefined : user.role"
>Role</gf-value
>
</div>
</div>
@ -52,7 +54,7 @@
size="medium"
[isDate]="true"
[locale]="data.locale"
[value]="user?.createdAt"
[value]="isLoading ? undefined : user.createdAt"
>Registration Date</gf-value
>
</div>
@ -61,7 +63,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="user?.provider"
[value]="isLoading ? undefined : user.provider"
>Authentication</gf-value
>
</div>
@ -74,7 +76,9 @@
i18n
size="medium"
[locale]="data.locale"
[value]="user?.subscription ? 'Premium' : 'Basic'"
[value]="
isLoading ? undefined : user.subscription ? 'Premium' : 'Basic'
"
>Membership</gf-value
>
</div>
@ -83,7 +87,11 @@
i18n
size="medium"
[value]="
user?.country ? getCountryName({ code: user.country }) : '-'
isLoading
? undefined
: user.country
? getCountryName({ code: user.country })
: '-'
"
>Country</gf-value
>
@ -97,7 +105,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="user?.accountCount"
[value]="isLoading ? undefined : user.accountCount"
>Accounts</gf-value
>
</div>
@ -106,7 +114,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="user?.activityCount"
[value]="isLoading ? undefined : user.activityCount"
>Activities</gf-value
>
</div>
@ -120,7 +128,7 @@
size="medium"
[locale]="data.locale"
[precision]="0"
[value]="user?.engagement"
[value]="isLoading ? undefined : user.engagement"
>Engagement per Day</gf-value
>
</div>
@ -129,7 +137,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="user?.dailyApiRequests"
[value]="isLoading ? undefined : user.dailyApiRequests"
>API Requests Today</gf-value
>
</div>

Loading…
Cancel
Save