Browse Source

Merge branch 'main' into 3453-biometric-toggle

pull/3496/head
Thomas Kaul 1 year ago
committed by GitHub
parent
commit
e1b8c70ea0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 4
      README.md
  3. 2
      apps/api/src/app/app.module.ts
  4. 29
      apps/api/src/app/asset/asset.controller.ts
  5. 17
      apps/api/src/app/asset/asset.module.ts
  6. 4
      apps/api/src/app/benchmark/benchmark.controller.ts
  7. 4
      apps/api/src/app/benchmark/benchmark.service.ts
  8. 18
      apps/client/localhost.cert
  9. 28
      apps/client/localhost.pem
  10. 5
      apps/client/project.json
  11. 4
      apps/client/src/app/components/home-market/home-market.component.ts
  12. 1
      apps/client/src/app/components/home-market/home-market.html
  13. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  14. 17
      apps/client/src/app/services/data.service.ts
  15. 2
      libs/common/src/lib/config.ts
  16. 2
      libs/common/src/lib/interfaces/benchmark.interface.ts
  17. 12
      libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.scss
  18. 87
      libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.ts
  19. 30
      libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html
  20. 11
      libs/ui/src/lib/benchmark/benchmark-detail-dialog/interfaces/interfaces.ts
  21. 12
      libs/ui/src/lib/benchmark/benchmark.component.html
  22. 75
      libs/ui/src/lib/benchmark/benchmark.component.ts

5
CHANGELOG.md

@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added a dialog for the benchmarks in the markets overview
### Changed ### Changed
- Improved the error handling in the biometric authentication registration - Improved the error handling in the biometric authentication registration
- Set up SSL for local development
## 2.89.0 - 2024-06-14 ## 2.89.0 - 2024-06-14

4
README.md

@ -161,7 +161,7 @@ Ghostfolio is available for various home server systems, including [CasaOS](http
1. Run `yarn database:setup` to initialize the database schema 1. Run `yarn database:setup` to initialize the database schema
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks 1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
1. Start the server and the client (see [_Development_](#Development)) 1. Start the server and the client (see [_Development_](#Development))
1. Open http://localhost:4200/en in your browser 1. Open https://localhost:4200/en in your browser
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`) 1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
### Start Server ### Start Server
@ -176,7 +176,7 @@ Run `yarn start:server`
### Start Client ### Start Client
Run `yarn start:client` and open http://localhost:4200/en in your browser Run `yarn start:client` and open https://localhost:4200/en in your browser
### Start _Storybook_ ### Start _Storybook_

2
apps/api/src/app/app.module.ts

@ -25,6 +25,7 @@ import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AssetModule } from './asset/asset.module';
import { AuthDeviceModule } from './auth-device/auth-device.module'; import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module'; import { BenchmarkModule } from './benchmark/benchmark.module';
@ -51,6 +52,7 @@ import { UserModule } from './user/user.module';
AdminModule, AdminModule,
AccessModule, AccessModule,
AccountModule, AccountModule,
AssetModule,
AuthDeviceModule, AuthDeviceModule,
AuthModule, AuthModule,
BenchmarkModule, BenchmarkModule,

29
apps/api/src/app/asset/asset.controller.ts

@ -0,0 +1,29 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { pick } from 'lodash';
@Controller('asset')
export class AssetController {
public constructor(private readonly adminService: AdminService) {}
@Get(':dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAsset(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AdminMarketDataDetails> {
const { assetProfile, marketData } =
await this.adminService.getMarketDataBySymbol({ dataSource, symbol });
return {
marketData,
assetProfile: pick(assetProfile, ['dataSource', 'name', 'symbol'])
};
}
}

17
apps/api/src/app/asset/asset.module.ts

@ -0,0 +1,17 @@
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { Module } from '@nestjs/common';
import { AssetController } from './asset.controller';
@Module({
controllers: [AssetController],
imports: [
AdminModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
]
})
export class AssetModule {}

4
apps/api/src/app/benchmark/benchmark.controller.ts

@ -105,7 +105,7 @@ export class BenchmarkController {
@Get(':dataSource/:symbol/:startDateString') @Get(':dataSource/:symbol/:startDateString')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getBenchmarkMarketDataBySymbol( public async getBenchmarkMarketDataForUser(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string, @Param('startDateString') startDateString: string,
@Param('symbol') symbol: string, @Param('symbol') symbol: string,
@ -117,7 +117,7 @@ export class BenchmarkController {
); );
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
return this.benchmarkService.getMarketDataBySymbol({ return this.benchmarkService.getMarketDataForUser({
dataSource, dataSource,
endDate, endDate,
startDate, startDate,

4
apps/api/src/app/benchmark/benchmark.service.ts

@ -153,6 +153,7 @@ export class BenchmarkService {
} }
return { return {
dataSource: benchmarkAssetProfiles[index].dataSource,
marketCondition: this.getMarketCondition( marketCondition: this.getMarketCondition(
performancePercentFromAllTimeHigh performancePercentFromAllTimeHigh
), ),
@ -163,6 +164,7 @@ export class BenchmarkService {
performancePercent: performancePercentFromAllTimeHigh performancePercent: performancePercentFromAllTimeHigh
} }
}, },
symbol: benchmarkAssetProfiles[index].symbol,
trend50d: benchmarkTrends[index].trend50d, trend50d: benchmarkTrends[index].trend50d,
trend200d: benchmarkTrends[index].trend200d trend200d: benchmarkTrends[index].trend200d
}; };
@ -213,7 +215,7 @@ export class BenchmarkService {
.sort((a, b) => a.name.localeCompare(b.name)); .sort((a, b) => a.name.localeCompare(b.name));
} }
public async getMarketDataBySymbol({ public async getMarketDataForUser({
dataSource, dataSource,
endDate = new Date(), endDate = new Date(),
startDate, startDate,

18
apps/client/localhost.cert

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC5TCCAc2gAwIBAgIJAJAMHOFnJ6oyMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAeFw0yNDAyMjcxNTQ2MzFaFw0yNDAzMjgxNTQ2MzFaMBQx
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAJ0hRjViikEKVIyukXR4CfuYVvFEFzB6AwAQ9Jrz2MseqpLacLTXFFAS54mp
iDuqPBzs9ta40mlSrqSBuAWKikpW5kTNnmqUnDZ6iSJezbYWx9YyULGqqb1q3C4/
5pH9m6NHJ+2uaUNKlDxYNKbntqs3drQEdxH9yv672Z53nvogTcf9jz6zjutEQGSV
HgVkCTTQmzf3+3st+VJ5D8JeYFR+tpZ6yleqgXFaTMtPZRfKLvTkQ+KeyCJLnsUJ
BQvdCKI0PGsG6d6ygXFmSePolD9KW3VTKKDPCsndID89vAnRWDj9UhzvycxmKpcF
GrUPH5+Pis1PM1R7OnAvnFygnyECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo
b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B
AQsFAAOCAQEAOdzrY3RTAPnBVAd3/GKIEkielYyOgKPnvd+RcOB+je8cXZY+vaxX
uEFP0526G00+kRZ2Tx9t0SmjoSpwg3lHUPMko0dIxgRlqISDAohdrEptHpcVujsD
ak88LLnAurr60JNjWX2wbEoJ18KLtqGSnATdmCgKwDPIN5a7+ssp44BGyzH6VYCg
wV3VjJl0pp4C5M0Jfu0p1FrQjzIVhgqR7JFYmvqIogWrGwYAQK/3XRXq8t48J5X3
IsfWiLAA2ZdCoWAnZ6PAGBOoGivtkJm967pHjd/28qYY6mQo4sN2ksEOjx6/YslF
2mOJdLV/DzqoggUsTpPEG0dRhzQLTGHKDQ==
-----END CERTIFICATE-----

28
apps/client/localhost.pem

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdIUY1YopBClSM
rpF0eAn7mFbxRBcwegMAEPSa89jLHqqS2nC01xRQEueJqYg7qjwc7PbWuNJpUq6k
gbgFiopKVuZEzZ5qlJw2eokiXs22FsfWMlCxqqm9atwuP+aR/ZujRyftrmlDSpQ8
WDSm57arN3a0BHcR/cr+u9med576IE3H/Y8+s47rREBklR4FZAk00Js39/t7LflS
eQ/CXmBUfraWespXqoFxWkzLT2UXyi705EPinsgiS57FCQUL3QiiNDxrBunesoFx
Zknj6JQ/Slt1UyigzwrJ3SA/PbwJ0Vg4/VIc78nMZiqXBRq1Dx+fj4rNTzNUezpw
L5xcoJ8hAgMBAAECggEAPU5YOEgELTA8oM8TjV+wdWuQsH2ilpVkSkhTR4nQkh+a
6cU0qDoqgLt/fySYNL9MyPRjso9V+SX7YdAC3paZMjwJh9q57lehQ1g33SMkG+Fz
gs0K0ucFZxQkaB8idN+ANAp1N7UO+ORGRe0cTeqmSNNRCxea5XgiFZVxaPS/IFOR
vVdXS1DulgwHh4H0ljKmkj7jc9zPBSc9ccW5Ml2q4a26Atu4IC/Mlp/DF4GNELbD
ebi9ZOZG33ip2bdhj3WX7NW9GJaaViKtVUpcpR6u+6BfvTXQ6xrqdoxXk9fnPzzf
sSoLPTt8yO4RavP1zQU22PhgIcWudhCiy/2Nv+uLqQKBgQDMPh1/xwdFl8yES8dy
f0xJyCDWPiB+xzGwcOAC90FrNZaqG0WWs3VHE4cilaWjCflvdR6aAEDEY68sZJhl
h+ChT/g61QLMOI+VIDQ1kJXKYgfS/B+BE1PZ0EkuymKFOvbNO8agHodB8CqnZaQh
bLuZaDnZ0JLK4im3KPt70+aKYwKBgQDE8s6xl0SLu+yJLo3VklCRD67Z8/jlvx2t
h3DF6NG8pB3QmvKdJWFKuLAWLGflJwbUW9Pt3hXkc0649KQrtiIYC0ZMh9KMaVCk
WmjS/n2eIUQZ7ZUlwFesi4p4iGynVBavIY8TJ6Y+K3TvsJgXP3IZ96r689PQJo8E
KbSeyYzFqwKBgGQTS4EAlJ+U8bEhMGj51veQCAbyChoUoFRD+n95h6RwbZKMKlzd
MenRt7VKfg6VJJNoX8Y1uYaBEaQ+5i1Zlsdz1718AhLu4+u+C9bzMXIo9ox63TTx
s3RWioVSxVNiwOtvDrQGQWAdvcioFPQLwyA34aDIgiTHDImimxbhjWThAoGAWOqW
Tq9QjxWk0Lpn5ohMP3GpK1VuhasnJvUDARb/uf8ORuPtrOz3Y9jGBvy9W0OnXbCn
mbiugZldbTtl8yYjdl+AuYSIlkPl2I3IzZl/9Shnqp0MvSJ9crT9KzXMeC8Knr6z
7Z30/BR6ksxTngtS5E5grzPt6Qe/gc2ich3kpEkCgYBfBHUhVIKVrDW/8a7U2679
Wj4i9us/M3iPMxcTv7/ZAg08TEvNBQYtvVQLu0NfDKXx8iGKJJ6evIYgNXVm2sPq
VzSgwGCALi1X0443amZU+mnX+/9RnBqyM+PJU8570mM83jqKlY/BEsi0aqxTioFG
an3xbjjN+Rq9iKLzmPxIMg==
-----END PRIVATE KEY-----

5
apps/client/project.json

@ -163,8 +163,11 @@
"serve": { "serve": {
"executor": "@nx/angular:dev-server", "executor": "@nx/angular:dev-server",
"options": { "options": {
"buildTarget": "client:build",
"proxyConfig": "apps/client/proxy.conf.json", "proxyConfig": "apps/client/proxy.conf.json",
"buildTarget": "client:build" "ssl": true,
"sslCert": "apps/client/localhost.cert",
"sslKey": "apps/client/localhost.pem"
}, },
"configurations": { "configurations": {
"development-de": { "development-de": {

4
apps/client/src/app/components/home-market/home-market.component.ts

@ -11,6 +11,7 @@ import {
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -21,6 +22,7 @@ import { takeUntil } from 'rxjs/operators';
}) })
export class HomeMarketComponent implements OnDestroy, OnInit { export class HomeMarketComponent implements OnDestroy, OnInit {
public benchmarks: Benchmark[]; public benchmarks: Benchmark[];
public deviceType: string;
public fearAndGreedIndex: number; public fearAndGreedIndex: number;
public fearLabel = $localize`Fear`; public fearLabel = $localize`Fear`;
public greedLabel = $localize`Greed`; public greedLabel = $localize`Greed`;
@ -36,8 +38,10 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.isLoading = true; this.isLoading = true;

1
apps/client/src/app/components/home-market/home-market.html

@ -32,6 +32,7 @@
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark <gf-benchmark
[benchmarks]="benchmarks" [benchmarks]="benchmarks"
[deviceType]="deviceType"
[locale]="user?.settings?.locale || undefined" [locale]="user?.settings?.locale || undefined"
[user]="user" [user]="user"
/> />

2
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -285,7 +285,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.isLoadingBenchmarkComparator = true; this.isLoadingBenchmarkComparator = true;
this.dataService this.dataService
.fetchBenchmarkBySymbol({ .fetchBenchmarkForUser({
dataSource, dataSource,
symbol, symbol,
range: this.user?.settings?.dateRange, range: this.user?.settings?.dateRange,

17
apps/client/src/app/services/data.service.ts

@ -19,6 +19,7 @@ import {
Access, Access,
AccountBalancesResponse, AccountBalancesResponse,
Accounts, Accounts,
AdminMarketDataDetails,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
Export, Export,
@ -284,7 +285,21 @@ export class DataService {
return this.http.get<Access[]>('/api/v1/access'); return this.http.get<Access[]>('/api/v1/access');
} }
public fetchBenchmarkBySymbol({ public fetchAsset({
dataSource,
symbol
}: UniqueAsset): Observable<AdminMarketDataDetails> {
return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe(
map((data) => {
for (const item of data.marketData) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public fetchBenchmarkForUser({
dataSource, dataSource,
range, range,
startDate, startDate,

2
libs/common/src/lib/config.ts

@ -41,7 +41,7 @@ export const DEFAULT_CURRENCY = 'USD';
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_LANGUAGE_CODE = 'en';
export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_ROOT_URL = 'http://localhost:4200'; export const DEFAULT_ROOT_URL = 'https://localhost:4200';
// USX is handled separately // USX is handled separately
export const DERIVED_CURRENCIES = [ export const DERIVED_CURRENCIES = [

2
libs/common/src/lib/interfaces/benchmark.interface.ts

@ -3,6 +3,7 @@ import { BenchmarkTrend } from '@ghostfolio/common/types/';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
export interface Benchmark { export interface Benchmark {
dataSource: EnhancedSymbolProfile['dataSource'];
marketCondition: 'ALL_TIME_HIGH' | 'BEAR_MARKET' | 'NEUTRAL_MARKET'; marketCondition: 'ALL_TIME_HIGH' | 'BEAR_MARKET' | 'NEUTRAL_MARKET';
name: EnhancedSymbolProfile['name']; name: EnhancedSymbolProfile['name'];
performances: { performances: {
@ -11,6 +12,7 @@ export interface Benchmark {
performancePercent: number; performancePercent: number;
}; };
}; };
symbol: EnhancedSymbolProfile['symbol'];
trend50d: BenchmarkTrend; trend50d: BenchmarkTrend;
trend200d: BenchmarkTrend; trend200d: BenchmarkTrend;
} }

12
libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.scss

@ -0,0 +1,12 @@
:host {
display: block;
.mat-mdc-dialog-content {
max-height: unset;
gf-line-chart {
aspect-ratio: 16 / 9;
margin: 0 -0.5rem;
}
}
}

87
libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.component.ts

@ -0,0 +1,87 @@
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AdminMarketDataDetails,
LineChartItem
} from '@ghostfolio/common/interfaces';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnDestroy,
OnInit
} from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef
} from '@angular/material/dialog';
import { format } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BenchmarkDetailDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfLineChartComponent,
MatDialogModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-benchmark-detail-dialog',
standalone: true,
styleUrls: ['./benchmark-detail-dialog.component.scss'],
templateUrl: 'benchmark-detail-dialog.html'
})
export class GfBenchmarkDetailDialogComponent implements OnDestroy, OnInit {
public assetProfile: AdminMarketDataDetails['assetProfile'];
public historicalDataItems: LineChartItem[];
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
public dialogRef: MatDialogRef<GfBenchmarkDetailDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: BenchmarkDetailDialogParams
) {}
public ngOnInit() {
this.dataService
.fetchAsset({
dataSource: this.data.dataSource,
symbol: this.data.symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ assetProfile, marketData }) => {
this.assetProfile = assetProfile;
this.historicalDataItems = marketData.map(({ date, marketPrice }) => {
return { date: format(date, DATE_FORMAT), value: marketPrice };
});
this.changeDetectorRef.markForCheck();
});
}
public onClose() {
this.dialogRef.close();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

30
libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html

@ -0,0 +1,30 @@
<gf-dialog-header
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="assetProfile?.name ?? assetProfile?.symbol"
(closeButtonClicked)="onClose()"
/>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<gf-line-chart
benchmarkLabel="Average Unit Price"
class="mb-4"
[colorScheme]="data.colorScheme"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="data.locale"
[showGradient]="true"
[showXAxis]="true"
[showYAxis]="true"
[symbol]="data.symbol"
/>
</div>
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>

11
libs/ui/src/lib/benchmark/benchmark-detail-dialog/interfaces/interfaces.ts

@ -0,0 +1,11 @@
import { ColorScheme } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client';
export interface BenchmarkDetailDialogParams {
colorScheme: ColorScheme;
dataSource: DataSource;
deviceType: string;
locale: string;
symbol: string;
}

12
libs/ui/src/lib/benchmark/benchmark.component.html

@ -110,5 +110,15 @@
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> <tr
*matRowDef="let row; columns: displayedColumns"
class="cursor-pointer"
mat-row
(click)="
onOpenBenchmarkDialog({
dataSource: row.dataSource,
symbol: row.symbol
})
"
></tr>
</table> </table>

75
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -1,6 +1,8 @@
import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper'; import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark, User } from '@ghostfolio/common/interfaces'; import { Benchmark, UniqueAsset, User } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
@ -8,13 +10,17 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
Input, Input,
OnChanges OnChanges,
OnDestroy
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
import { GfTrendIndicatorComponent } from '../trend-indicator'; import { GfBenchmarkDetailDialogComponent } from './benchmark-detail-dialog/benchmark-detail-dialog.component';
import { GfValueComponent } from '../value'; import { BenchmarkDetailDialogParams } from './benchmark-detail-dialog/interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -23,7 +29,8 @@ import { GfValueComponent } from '../value';
GfTrendIndicatorComponent, GfTrendIndicatorComponent,
GfValueComponent, GfValueComponent,
MatTableModule, MatTableModule,
NgxSkeletonLoaderModule NgxSkeletonLoaderModule,
RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-benchmark', selector: 'gf-benchmark',
@ -31,8 +38,9 @@ import { GfValueComponent } from '../value';
styleUrls: ['./benchmark.component.scss'], styleUrls: ['./benchmark.component.scss'],
templateUrl: './benchmark.component.html' templateUrl: './benchmark.component.html'
}) })
export class GfBenchmarkComponent implements OnChanges { export class GfBenchmarkComponent implements OnChanges, OnDestroy {
@Input() benchmarks: Benchmark[]; @Input() benchmarks: Benchmark[];
@Input() deviceType: string;
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() user: User; @Input() user: User;
@ -40,7 +48,28 @@ export class GfBenchmarkComponent implements OnChanges {
public resolveMarketCondition = resolveMarketCondition; public resolveMarketCondition = resolveMarketCondition;
public translate = translate; public translate = translate;
public constructor() {} private unsubscribeSubject = new Subject<void>();
public constructor(
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router
) {
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
params['benchmarkDetailDialog'] &&
params['dataSource'] &&
params['symbol']
) {
this.openBenchmarkDetailDialog({
dataSource: params['dataSource'],
symbol: params['symbol']
});
}
});
}
public ngOnChanges() { public ngOnChanges() {
if (this.user?.settings?.isExperimentalFeatures) { if (this.user?.settings?.isExperimentalFeatures) {
@ -54,4 +83,36 @@ export class GfBenchmarkComponent implements OnChanges {
]; ];
} }
} }
public onOpenBenchmarkDialog({ dataSource, symbol }: UniqueAsset) {
this.router.navigate([], {
queryParams: { dataSource, symbol, benchmarkDetailDialog: true }
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openBenchmarkDetailDialog({ dataSource, symbol }: UniqueAsset) {
const dialogRef = this.dialog.open(GfBenchmarkDetailDialogComponent, {
data: <BenchmarkDetailDialogParams>{
dataSource,
symbol,
colorScheme: this.user?.settings?.colorScheme,
deviceType: this.deviceType,
locale: this.locale
},
height: this.deviceType === 'mobile' ? '97.5vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
} }

Loading…
Cancel
Save