From 73c5ff37f35e413d959ad6d9ae7f4ba05128b30c Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 Aug 2022 20:30:04 +0200 Subject: [PATCH] Add asset sub class filter --- apps/api/src/app/admin/admin.controller.ts | 21 +++++- apps/api/src/app/admin/admin.service.ts | 27 ++++++-- .../admin-market-data.component.ts | 68 +++++++++++++------ .../admin-market-data/admin-market-data.html | 10 +++ .../admin-market-data.module.ts | 2 + .../assset-profile-dialog.module.ts | 2 +- apps/client/src/app/services/data.service.ts | 28 +++++++- .../src/lib/interfaces/filter.interface.ts | 2 +- 8 files changed, 129 insertions(+), 31 deletions(-) diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index b13ba5b5e..e7209fa01 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -8,7 +8,8 @@ import { import { AdminData, AdminMarketData, - AdminMarketDataDetails + AdminMarketDataDetails, + Filter } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -22,6 +23,7 @@ import { Param, Post, Put, + Query, UseGuards } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; @@ -226,7 +228,9 @@ export class AdminController { @Get('market-data') @UseGuards(AuthGuard('jwt')) - public async getMarketData(): Promise { + public async getMarketData( + @Query('assetSubClasses') filterByAssetSubClasses?: string + ): Promise { if ( !hasPermission( this.request.user.permissions, @@ -239,7 +243,18 @@ export class AdminController { ); } - return this.adminService.getMarketData(); + const assetSubClasses = filterByAssetSubClasses?.split(',') ?? []; + + const filters: Filter[] = [ + ...assetSubClasses.map((assetSubClass) => { + return { + id: assetSubClass, + type: 'ASSET_SUB_CLASS' + }; + }) + ]; + + return this.adminService.getMarketData(filters); } @Get('market-data/:dataSource/:symbol') diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index cbbcda7e6..42ee800ba 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -11,11 +11,13 @@ import { AdminMarketData, AdminMarketDataDetails, AdminMarketDataItem, + Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; -import { Property } from '@prisma/client'; +import { AssetSubClass, Prisma, Property } from '@prisma/client'; import { differenceInDays } from 'date-fns'; +import { groupBy } from 'lodash'; @Injectable() export class AdminService { @@ -63,14 +65,27 @@ export class AdminService { }; } - public async getMarketData(): Promise { + public async getMarketData(filters?: Filter[]): Promise { + const where: Prisma.SymbolProfileWhereInput = {}; + + const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( + filters, + (filter) => { + return filter.type; + } + ); + const marketData = await this.prismaService.marketData.groupBy({ _count: true, by: ['dataSource', 'symbol'] }); - const currencyPairsToGather: AdminMarketDataItem[] = - this.exchangeRateDataService + let currencyPairsToGather: AdminMarketDataItem[] = []; + + if (filtersByAssetSubClass) { + where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; + } else { + currencyPairsToGather = this.exchangeRateDataService .getCurrencyPairs() .map(({ dataSource, symbol }) => { const marketDataItemCount = @@ -89,9 +104,11 @@ export class AdminService { sectorsCount: 0 }; }); + } const symbolProfilesToGather: AdminMarketDataItem[] = ( await this.prismaService.symbolProfile.findMany({ + where, orderBy: [{ symbol: 'asc' }], select: { _count: { @@ -100,7 +117,6 @@ export class AdminService { assetClass: true, assetSubClass: true, countries: true, - sectors: true, dataSource: true, Order: { orderBy: [{ date: 'asc' }], @@ -108,6 +124,7 @@ export class AdminService { take: 1 }, scraperConfiguration: true, + sectors: true, symbol: true } }) diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index fdc8b544e..34e6c91ce 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -14,13 +14,14 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DATE_FORMAT, getDateFormatString } from '@ghostfolio/common/helper'; -import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; +import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; -import { DataSource } from '@prisma/client'; +import { AssetSubClass, DataSource } from '@prisma/client'; import { format, parseISO } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; + import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component'; import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces'; @@ -33,9 +34,27 @@ import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/inte export class AdminMarketDataComponent implements OnDestroy, OnInit { @ViewChild(MatSort) sort: MatSort; + public activeFilters: Filter[] = []; + public allFilters: Filter[] = [ + AssetSubClass.BOND, + AssetSubClass.COMMODITY, + AssetSubClass.CRYPTOCURRENCY, + AssetSubClass.ETF, + AssetSubClass.MUTUALFUND, + AssetSubClass.PRECIOUS_METAL, + AssetSubClass.PRIVATE_EQUITY, + AssetSubClass.STOCK + ].map((id) => { + return { + id, + label: id, + type: 'ASSET_SUB_CLASS' + }; + }); public currentDataSource: DataSource; public currentSymbol: string; - public dataSource: MatTableDataSource = new MatTableDataSource(); + public dataSource: MatTableDataSource = + new MatTableDataSource(); public defaultDateFormat: string; public deviceType: string; public displayedColumns = [ @@ -50,7 +69,9 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { 'sectorsCount', 'actions' ]; - public marketData: AdminMarketDataItem[] = []; + public filters$ = new Subject(); + public isLoading = false; + public placeholder = ''; public user: User; private unsubscribeSubject = new Subject(); @@ -98,7 +119,29 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; - this.fetchAdminMarketData(); + this.filters$ + .pipe( + distinctUntilChanged(), + switchMap((filters) => { + this.isLoading = true; + this.activeFilters = filters; + this.placeholder = + this.activeFilters.length <= 0 ? $localize`Filter by...` : ''; + + return this.dataService.fetchAdminMarketData({ + filters: this.activeFilters + }); + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe(({ marketData }) => { + this.dataSource = new MatTableDataSource(marketData); + this.dataSource.sort = this.sort; + + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); } public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { @@ -142,19 +185,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private fetchAdminMarketData() { - this.dataService - .fetchAdminMarketData() - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ marketData }) => { - this.dataSource = new MatTableDataSource(marketData); - - this.dataSource.sort = this.sort; - - this.changeDetectorRef.markForCheck(); - }); - } - private openAssetProfileDialog({ dataSource, dateOfFirstActivity, diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index d116a0e23..5ae602b3c 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -1,4 +1,14 @@
+
+
+ +
+
('/api/v1/admin'); } - public fetchAdminMarketData() { - return this.http.get('/api/v1/admin/market-data'); + public fetchAdminMarketData({ filters }: { filters?: Filter[] }) { + let params = new HttpParams(); + + if (filters?.length > 0) { + const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( + filters, + (filter) => { + return filter.type; + } + ); + + if (filtersByAssetSubClass) { + params = params.append( + 'assetSubClasses', + filtersByAssetSubClass + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + } + + return this.http.get('/api/v1/admin/market-data', { + params + }); } public deleteAccess(aId: string) { diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index a3e5163e7..6695de883 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -1,5 +1,5 @@ export interface Filter { id: string; label?: string; - type: 'ACCOUNT' | 'ASSET_CLASS' | 'SYMBOL' | 'TAG'; + type: 'ACCOUNT' | 'ASSET_CLASS' | 'ASSET_SUB_CLASS' | 'SYMBOL' | 'TAG'; }