Browse Source

Feature/add frontend for watchlist (#4604)

* Add frontend for watchlist

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
pull/4616/head
Kenrick Tandrian 2 days ago
committed by GitHub
parent
commit
c671ea4022
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 22
      apps/api/src/app/endpoints/watchlist/watchlist.controller.ts
  3. 3
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.scss
  4. 92
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.ts
  5. 25
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.html
  6. 4
      apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/interfaces/interfaces.ts
  7. 145
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  8. 33
      apps/client/src/app/components/home-watchlist/home-watchlist.html
  9. 3
      apps/client/src/app/components/home-watchlist/home-watchlist.scss
  10. 6
      apps/client/src/app/pages/home/home-page-routing.module.ts
  11. 6
      apps/client/src/app/pages/home/home-page.component.ts
  12. 2
      apps/client/src/app/pages/home/home-page.module.ts
  13. 12
      apps/client/src/app/services/data.service.ts
  14. 2
      libs/common/src/lib/interfaces/index.ts
  15. 5
      libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts
  16. 4
      libs/ui/src/lib/benchmark/benchmark.component.html
  17. 2
      libs/ui/src/lib/benchmark/benchmark.component.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Introduced a watchlist to follow assets (experimental)
## 2.156.0 - 2025-04-27 ## 2.156.0 - 2025-04-27
### Changed ### Changed

22
apps/api/src/app/endpoints/watchlist/watchlist.controller.ts

@ -2,7 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { WatchlistResponse } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
@ -53,13 +53,13 @@ export class WatchlistController {
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
) { ) {
const watchlistItem = await this.watchlistService const watchlistItems = await this.watchlistService.getWatchlistItems(
.getWatchlistItems(this.request.user.id) this.request.user.id
.then((items) => { );
return items.find((item) => {
const watchlistItem = watchlistItems.find((item) => {
return item.dataSource === dataSource && item.symbol === symbol; return item.dataSource === dataSource && item.symbol === symbol;
}); });
});
if (!watchlistItem) { if (!watchlistItem) {
throw new HttpException( throw new HttpException(
@ -79,7 +79,13 @@ export class WatchlistController {
@HasPermission(permissions.readWatchlist) @HasPermission(permissions.readWatchlist)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getWatchlistItems(): Promise<AssetProfileIdentifier[]> { public async getWatchlistItems(): Promise<WatchlistResponse> {
return this.watchlistService.getWatchlistItems(this.request.user.id); const watchlist = await this.watchlistService.getWatchlistItems(
this.request.user.id
);
return {
watchlist
};
} }
} }

3
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

92
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.component.ts

@ -0,0 +1,92 @@
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
OnDestroy,
OnInit
} from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { Subject } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'h-100' },
imports: [
CommonModule,
FormsModule,
GfSymbolAutocompleteComponent,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
ReactiveFormsModule
],
selector: 'gf-create-watchlist-item-dialog',
styleUrls: ['./create-watchlist-item-dialog.component.scss'],
templateUrl: 'create-watchlist-item-dialog.html'
})
export class CreateWatchlistItemDialogComponent implements OnInit, OnDestroy {
public createWatchlistItemForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
public constructor(
public readonly dialogRef: MatDialogRef<CreateWatchlistItemDialogComponent>,
public readonly formBuilder: FormBuilder
) {}
public ngOnInit() {
this.createWatchlistItemForm = this.formBuilder.group(
{
searchSymbol: new FormControl(null, [Validators.required])
},
{
validators: this.validator
}
);
}
public onCancel() {
this.dialogRef.close();
}
public onSubmit() {
this.dialogRef.close({
dataSource:
this.createWatchlistItemForm.get('searchSymbol').value.dataSource,
symbol: this.createWatchlistItemForm.get('searchSymbol').value.symbol
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private validator(control: AbstractControl): ValidationErrors {
const searchSymbolControl = control.get('searchSymbol');
if (
searchSymbolControl.valid &&
searchSymbolControl.value.dataSource &&
searchSymbolControl.value.symbol
) {
return { incomplete: false };
}
return { incomplete: true };
}
}

25
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.html

@ -0,0 +1,25 @@
<form
class="d-flex flex-column h-100"
[formGroup]="createWatchlistItemForm"
(keyup.enter)="createWatchlistItemForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<h1 i18n mat-dialog-title>Add asset to watchlist</h1>
<div class="flex-grow-1 py-3" mat-dialog-content>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label>
<gf-symbol-autocomplete formControlName="searchSymbol" />
</mat-form-field>
</div>
<div class="d-flex justify-content-end" mat-dialog-actions>
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="createWatchlistItemForm.hasError('incomplete')"
>
<ng-container i18n>Save</ng-container>
</button>
</div>
</form>

4
apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/interfaces/interfaces.ts

@ -0,0 +1,4 @@
export interface CreateWatchlistItemDialogParams {
deviceType: string;
locale: string;
}

145
apps/client/src/app/components/home-watchlist/home-watchlist.component.ts

@ -0,0 +1,145 @@
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Benchmark, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnDestroy,
OnInit
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { CreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component';
import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
GfBenchmarkComponent,
GfPremiumIndicatorComponent,
MatButtonModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-watchlist',
styleUrls: ['./home-watchlist.scss'],
templateUrl: './home-watchlist.html'
})
export class HomeWatchlistComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasPermissionToCreateWatchlistItem: boolean;
public user: User;
public watchlist: Benchmark[];
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createWatchlistItemDialog']) {
this.openCreateWatchlistItemDialog();
}
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToCreateWatchlistItem = hasPermission(
this.user.permissions,
permissions.createWatchlistItem
);
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.loadWatchlistData();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private loadWatchlistData() {
this.dataService
.fetchWatchlist()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ watchlist }) => {
this.watchlist = watchlist.map(({ dataSource, symbol }) => ({
dataSource,
symbol,
marketCondition: null,
name: symbol,
performances: null,
trend50d: 'UNKNOWN',
trend200d: 'UNKNOWN'
}));
this.changeDetectorRef.markForCheck();
});
}
private openCreateWatchlistItemDialog() {
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
const dialogRef = this.dialog.open(CreateWatchlistItemDialogComponent, {
autoFocus: false,
data: {
deviceType: this.deviceType,
locale: this.user?.settings?.locale
} as CreateWatchlistItemDialogParams,
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ dataSource, symbol } = {}) => {
if (dataSource && symbol) {
this.dataService
.postWatchlistItem({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => this.loadWatchlistData()
});
}
this.router.navigate(['.'], { relativeTo: this.route });
});
});
}
}

33
apps/client/src/app/components/home-watchlist/home-watchlist.html

@ -0,0 +1,33 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-4">
<span class="align-items-center d-flex justify-content-center">
<span i18n>Watchlist</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</span>
</h1>
<div class="mb-3 row">
<div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark
[benchmarks]="watchlist"
[deviceType]="deviceType"
[locale]="user?.settings?.locale || undefined"
[user]="user"
/>
</div>
</div>
</div>
@if (hasPermissionToCreateWatchlistItem) {
<div class="fab-container">
<a
class="align-items-center d-flex justify-content-center"
color="primary"
mat-fab
[queryParams]="{ createWatchlistItemDialog: true }"
[routerLink]="[]"
>
<ion-icon name="add-outline" size="large" />
</a>
</div>
}

3
apps/client/src/app/components/home-watchlist/home-watchlist.scss

@ -0,0 +1,3 @@
:host {
display: block;
}

6
apps/client/src/app/pages/home/home-page-routing.module.ts

@ -2,6 +2,7 @@ import { HomeHoldingsComponent } from '@ghostfolio/client/components/home-holdin
import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component'; import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component'; import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component'; import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
@ -36,6 +37,11 @@ const routes: Routes = [
path: 'market', path: 'market',
component: HomeMarketComponent, component: HomeMarketComponent,
title: $localize`Markets` title: $localize`Markets`
},
{
path: 'watchlist',
component: HomeWatchlistComponent,
title: $localize`Watchlist`
} }
], ],
component: HomePageComponent, component: HomePageComponent,

6
apps/client/src/app/pages/home/home-page.component.ts

@ -52,6 +52,12 @@ export class HomePageComponent implements OnDestroy, OnInit {
iconName: 'newspaper-outline', iconName: 'newspaper-outline',
label: $localize`Markets`, label: $localize`Markets`,
path: ['/home', 'market'] path: ['/home', 'market']
},
{
iconName: 'star-outline',
label: $localize`Watchlist`,
path: ['/home', 'watchlist'],
showCondition: this.user?.settings?.isExperimentalFeatures
} }
]; ];
this.user = state.user; this.user = state.user;

2
apps/client/src/app/pages/home/home-page.module.ts

@ -2,6 +2,7 @@ import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holding
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module'; import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
import { GfHomeOverviewModule } from '@ghostfolio/client/components/home-overview/home-overview.module'; import { GfHomeOverviewModule } from '@ghostfolio/client/components/home-overview/home-overview.module';
import { GfHomeSummaryModule } from '@ghostfolio/client/components/home-summary/home-summary.module'; import { GfHomeSummaryModule } from '@ghostfolio/client/components/home-summary/home-summary.module';
import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@ -20,6 +21,7 @@ import { HomePageComponent } from './home-page.component';
GfHomeOverviewModule, GfHomeOverviewModule,
GfHomeSummaryModule, GfHomeSummaryModule,
HomePageRoutingModule, HomePageRoutingModule,
HomeWatchlistComponent,
MatTabsModule, MatTabsModule,
RouterModule RouterModule
], ],

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

@ -6,6 +6,7 @@ import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto'; import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto'; import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
import { CreateWatchlistItemDto } from '@ghostfolio/api/app/endpoints/watchlist/create-watchlist-item.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { import {
Activities, Activities,
@ -44,7 +45,8 @@ import {
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioReportResponse, PortfolioReportResponse,
PublicPortfolioResponse, PublicPortfolioResponse,
User User,
WatchlistResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import type { import type {
@ -686,6 +688,10 @@ export class DataService {
return this.http.get<Tag[]>('/api/v1/tags'); return this.http.get<Tag[]>('/api/v1/tags');
} }
public fetchWatchlist() {
return this.http.get<WatchlistResponse>('/api/v1/watchlist');
}
public generateAccessToken(aUserId: string) { public generateAccessToken(aUserId: string) {
return this.http.post<AccessTokenResponse>( return this.http.post<AccessTokenResponse>(
`/api/v1/user/${aUserId}/access-token`, `/api/v1/user/${aUserId}/access-token`,
@ -748,6 +754,10 @@ export class DataService {
return this.http.post<UserItem>('/api/v1/user', {}); return this.http.post<UserItem>('/api/v1/user', {});
} }
public postWatchlistItem(watchlistItem: CreateWatchlistItemDto) {
return this.http.post('/api/v1/watchlist', watchlistItem);
}
public putAccount(aAccount: UpdateAccountDto) { public putAccount(aAccount: UpdateAccountDto) {
return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount); return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
} }

2
libs/common/src/lib/interfaces/index.ts

@ -57,6 +57,7 @@ import type { PortfolioPerformanceResponse } from './responses/portfolio-perform
import type { PortfolioReportResponse } from './responses/portfolio-report.interface'; import type { PortfolioReportResponse } from './responses/portfolio-report.interface';
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface'; import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
import type { QuotesResponse } from './responses/quotes-response.interface'; import type { QuotesResponse } from './responses/quotes-response.interface';
import type { WatchlistResponse } from './responses/watchlist-response.interface';
import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface';
import type { Statistics } from './statistics.interface'; import type { Statistics } from './statistics.interface';
import type { SubscriptionOffer } from './subscription-offer.interface'; import type { SubscriptionOffer } from './subscription-offer.interface';
@ -135,5 +136,6 @@ export {
ToggleOption, ToggleOption,
User, User,
UserSettings, UserSettings,
WatchlistResponse,
XRayRulesSettings XRayRulesSettings
}; };

5
libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts

@ -0,0 +1,5 @@
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
export interface WatchlistResponse {
watchlist: AssetProfileIdentifier[];
}

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

@ -66,11 +66,13 @@
</th> </th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@if (element?.performances?.allTimeHigh?.date) {
<gf-value <gf-value
[isDate]="true" [isDate]="true"
[locale]="locale" [locale]="locale"
[value]="element?.performances?.allTimeHigh?.date" [value]="element?.performances?.allTimeHigh?.date"
/> />
}
</div> </div>
</td> </td>
</ng-container> </ng-container>
@ -83,6 +85,7 @@
<span class="d-block d-sm-none text-nowrap" i18n>from ATH</span> <span class="d-block d-sm-none text-nowrap" i18n>from ATH</span>
</th> </th>
<td *matCellDef="let element" class="px-2 text-right" mat-cell> <td *matCellDef="let element" class="px-2 text-right" mat-cell>
@if (isNumber(element?.performances?.allTimeHigh?.performancePercent)) {
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isPercent]="true" [isPercent]="true"
@ -95,6 +98,7 @@
}" }"
[value]="element?.performances?.allTimeHigh?.performancePercent" [value]="element?.performances?.allTimeHigh?.performancePercent"
/> />
}
</td> </td>
</ng-container> </ng-container>

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

@ -20,6 +20,7 @@ import {
import { MatDialog } from '@angular/material/dialog'; 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 { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
@ -49,6 +50,7 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
public displayedColumns = ['name', 'date', 'change', 'marketCondition']; public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
public isLoading = true; public isLoading = true;
public isNumber = isNumber;
public resolveMarketCondition = resolveMarketCondition; public resolveMarketCondition = resolveMarketCondition;
public translate = translate; public translate = translate;

Loading…
Cancel
Save