From b1a2aba750bfd8c1e32e5dc59e01a8ddfc9f03af Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Thu, 29 Jun 2023 21:34:36 +0200 Subject: [PATCH] Add pagination --- apps/api/src/app/admin/admin.controller.ts | 11 +- apps/api/src/app/admin/admin.service.ts | 117 ++++++++------- .../admin-market-data.component.ts | 75 ++++++---- .../admin-market-data/admin-market-data.html | 38 +++-- .../admin-market-data.module.ts | 4 + .../admin-overview.component.ts | 4 +- .../admin-users/admin-users.component.ts | 4 +- apps/client/src/app/services/admin.service.ts | 35 ++++- apps/client/src/app/services/data.service.ts | 134 ++++++++---------- .../interfaces/admin-market-data.interface.ts | 1 + .../activities-table.component.html | 2 +- 11 files changed, 252 insertions(+), 173 deletions(-) diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 6512384ba..626ecc410 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -3,6 +3,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { + DEFAULT_PAGE_SIZE, GATHER_ASSET_PROFILE_PROCESS, GATHER_ASSET_PROFILE_PROCESS_OPTIONS } from '@ghostfolio/common/config'; @@ -247,7 +248,9 @@ export class AdminController { @Get('market-data') @UseGuards(AuthGuard('jwt')) public async getMarketData( - @Query('assetSubClasses') filterByAssetSubClasses?: string + @Query('assetSubClasses') filterByAssetSubClasses?: string, + @Query('skip') skip?: number, + @Query('take') take?: number ): Promise { if ( !hasPermission( @@ -272,7 +275,11 @@ export class AdminController { }) ]; - return this.adminService.getMarketData(filters); + return this.adminService.getMarketData({ + filters, + skip: isNaN(skip) ? undefined : skip, + take: isNaN(take) ? undefined : take + }); } @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 9a6f1bf17..d68d4d1e4 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -6,12 +6,14 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; -import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; +import { + DEFAULT_PAGE_SIZE, + PROPERTY_CURRENCIES +} from '@ghostfolio/common/config'; import { AdminData, AdminMarketData, AdminMarketDataDetails, - AdminMarketDataItem, Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; @@ -99,7 +101,15 @@ export class AdminService { }; } - public async getMarketData(filters?: Filter[]): Promise { + public async getMarketData({ + filters, + skip, + take = DEFAULT_PAGE_SIZE + }: { + filters?: Filter[]; + skip?: number; + take?: number; + }): Promise { const where: Prisma.SymbolProfileWhereInput = {}; const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( @@ -109,40 +119,19 @@ export class AdminService { } ); - const marketData = await this.prismaService.marketData.groupBy({ + const marketDataItems = await this.prismaService.marketData.groupBy({ _count: true, by: ['dataSource', 'symbol'] }); - let currencyPairsToGather: AdminMarketDataItem[] = []; - if (filtersByAssetSubClass) { where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; - } else { - currencyPairsToGather = this.exchangeRateDataService - .getCurrencyPairs() - .map(({ dataSource, symbol }) => { - const marketDataItemCount = - marketData.find((marketDataItem) => { - return ( - marketDataItem.dataSource === dataSource && - marketDataItem.symbol === symbol - ); - })?._count ?? 0; - - return { - dataSource, - marketDataItemCount, - symbol, - assetClass: 'CASH', - countriesCount: 0, - sectorsCount: 0 - }; - }); } - const symbolProfilesToGather: AdminMarketDataItem[] = ( - await this.prismaService.symbolProfile.findMany({ + const [assetProfiles, count] = await Promise.all([ + this.prismaService.symbolProfile.findMany({ + skip, + take, where, orderBy: [{ symbol: 'asc' }], select: { @@ -163,38 +152,48 @@ export class AdminService { sectors: true, symbol: true } - }) - ).map((symbolProfile) => { - const countriesCount = symbolProfile.countries - ? Object.keys(symbolProfile.countries).length - : 0; - const marketDataItemCount = - marketData.find((marketDataItem) => { - return ( - marketDataItem.dataSource === symbolProfile.dataSource && - marketDataItem.symbol === symbolProfile.symbol - ); - })?._count ?? 0; - const sectorsCount = symbolProfile.sectors - ? Object.keys(symbolProfile.sectors).length - : 0; - - return { - countriesCount, - marketDataItemCount, - sectorsCount, - activitiesCount: symbolProfile._count.Order, - assetClass: symbolProfile.assetClass, - assetSubClass: symbolProfile.assetSubClass, - comment: symbolProfile.comment, - dataSource: symbolProfile.dataSource, - date: symbolProfile.Order?.[0]?.date, - symbol: symbolProfile.symbol - }; - }); + }), + this.prismaService.symbolProfile.count({ where }) + ]); return { - marketData: [...currencyPairsToGather, ...symbolProfilesToGather] + count, + marketData: assetProfiles.map( + ({ + _count, + assetClass, + assetSubClass, + comment, + countries, + dataSource, + Order, + sectors, + symbol + }) => { + const countriesCount = countries ? Object.keys(countries).length : 0; + const marketDataItemCount = + marketDataItems.find((marketDataItem) => { + return ( + marketDataItem.dataSource === dataSource && + marketDataItem.symbol === symbol + ); + })?._count ?? 0; + const sectorsCount = sectors ? Object.keys(sectors).length : 0; + + return { + assetClass, + assetSubClass, + comment, + countriesCount, + dataSource, + symbol, + marketDataItemCount, + sectorsCount, + activitiesCount: _count.Order, + date: Order?.[0]?.date + }; + } + ) }; } 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 6ce2acd1f..6cca44f59 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 @@ -11,7 +11,6 @@ import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; 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 { getDateFormatString } from '@ghostfolio/common/helper'; import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces'; @@ -26,6 +25,8 @@ import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog. import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces'; import { CreateAssetProfileDialog } from './create-asset-profile-dialog/create-asset-profile-dialog.component'; import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/interfaces/interfaces'; +import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -34,6 +35,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in templateUrl: './admin-market-data.html' }) export class AdminMarketDataComponent implements OnDestroy, OnInit { + @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; public activeFilters: Filter[] = []; @@ -75,6 +77,8 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { public filters$ = new Subject(); public isLoading = false; public placeholder = ''; + public pageSize = DEFAULT_PAGE_SIZE; + public totalItems = 0; public user: User; private unsubscribeSubject = new Subject(); @@ -82,7 +86,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, private route: ActivatedRoute, @@ -117,34 +120,22 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { ); } }); + + this.filters$ + .pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject)) + .subscribe((filters) => { + this.activeFilters = filters; + + this.loadData(); + }); } public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; + } - 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 onChangePage(page: PageEvent) { + this.loadData(page.pageIndex); } public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { @@ -212,6 +203,35 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } + private loadData(aPageIndex = 0) { + this.isLoading = true; + + if (aPageIndex === 0 && this.paginator) { + this.paginator.pageIndex = 0; + } + + this.placeholder = + this.activeFilters.length <= 0 ? $localize`Filter by...` : ''; + + this.adminService + .fetchAdminMarketData({ + filters: this.activeFilters, + skip: aPageIndex * this.pageSize, + take: this.pageSize + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ count, marketData }) => { + this.totalItems = count; + + this.dataSource = new MatTableDataSource(marketData); + this.dataSource.sort = this.sort; + + this.isLoading = false; + + this.changeDetectorRef.markForCheck(); + }); + } + private openAssetProfileDialog({ dataSource, symbol @@ -274,8 +294,9 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.isLoading = true; this.changeDetectorRef.markForCheck(); - return this.dataService.fetchAdminMarketData({ - filters: this.activeFilters + return this.adminService.fetchAdminMarketData({ + filters: this.activeFilters, + take: this.pageSize }); }), takeUntil(this.unsubscribeSubject) 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 728d32e8c..39fd67698 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 @@ -29,7 +29,7 @@ - + Data Source @@ -38,7 +38,7 @@ - + Asset Class @@ -47,7 +47,7 @@ - + Asset Sub Class @@ -56,7 +56,7 @@ - + First Activity @@ -65,7 +65,7 @@ - + Activities Count @@ -74,7 +74,7 @@ - + Historical Data @@ -83,7 +83,7 @@ - + Sectors Count @@ -92,7 +92,7 @@ - + Countries Count @@ -162,6 +162,28 @@ (click)="onOpenAssetProfileDialog({ dataSource: row.dataSource, symbol: row.symbol })" > + + + + diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts index ffb3d4ee2..060e8a6b0 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts @@ -2,10 +2,12 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { RouterModule } from '@angular/router'; import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { AdminMarketDataComponent } from './admin-market-data.component'; import { GfAssetProfileDialogModule } from './asset-profile-dialog/asset-profile-dialog.module'; @@ -20,8 +22,10 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/ GfCreateAssetProfileDialogModule, MatButtonModule, MatMenuModule, + MatPaginatorModule, MatSortModule, MatTableModule, + NgxSkeletonLoaderModule, RouterModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 5aaa3e518..2053c4298 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; @@ -45,6 +46,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { private unsubscribeSubject = new Subject(); public constructor( + private adminService: AdminService, private cacheService: CacheService, private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, @@ -197,7 +199,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { } private fetchAdminData() { - this.dataService + this.adminService .fetchAdminData() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ exchangeRates, settings, transactionCount, userCount }) => { diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 3b5ae9758..48783c91b 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +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 { UserService } from '@ghostfolio/client/services/user/user.service'; @@ -30,6 +31,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { private unsubscribeSubject = new Subject(); public constructor( + private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private impersonationStorageService: ImpersonationStorageService, @@ -112,7 +114,7 @@ export class AdminUsersComponent implements OnDestroy, OnInit { } private fetchAdminData() { - this.dataService + this.adminService .fetchAdminData() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ users }) => { diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 8b126898f..7755137d1 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -7,21 +7,28 @@ import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform. import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { + AdminData, AdminJobs, + AdminMarketData, AdminMarketDataDetails, EnhancedSymbolProfile, + Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; import { DataSource, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; import { format, parseISO } from 'date-fns'; import { Observable, map } from 'rxjs'; +import { DataService } from './data.service'; @Injectable({ providedIn: 'root' }) export class AdminService { - public constructor(private http: HttpClient) {} + public constructor( + private dataService: DataService, + private http: HttpClient + ) {} public addAssetProfile({ dataSource, symbol }: UniqueAsset) { return this.http.post( @@ -56,6 +63,32 @@ export class AdminService { ); } + public fetchAdminData() { + return this.http.get('/api/v1/admin'); + } + + public fetchAdminMarketData({ + filters, + skip, + take + }: { + filters?: Filter[]; + skip?: number; + take: number; + }) { + let params = this.dataService.buildFiltersAsQueryParams({ filters }); + + if (skip) { + params = params.append('skip', skip); + } + + params = params.append('take', take); + + return this.http.get('/api/v1/admin/market-data', { + params + }); + } + public fetchAdminMarketDataBySymbol({ dataSource, symbol diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 4aa590493..f2e5169db 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -18,8 +18,6 @@ import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Access, Accounts, - AdminData, - AdminMarketData, BenchmarkMarketDataDetails, BenchmarkResponse, Export, @@ -51,6 +49,67 @@ import { map } from 'rxjs/operators'; export class DataService { public constructor(private http: HttpClient) {} + public buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) { + let params = new HttpParams(); + + if (filters?.length > 0) { + const { + ACCOUNT: filtersByAccount, + ASSET_CLASS: filtersByAssetClass, + ASSET_SUB_CLASS: filtersByAssetSubClass, + TAG: filtersByTag + } = groupBy(filters, (filter) => { + return filter.type; + }); + + if (filtersByAccount) { + params = params.append( + 'accounts', + filtersByAccount + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByAssetClass) { + params = params.append( + 'assetClasses', + filtersByAssetClass + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByAssetSubClass) { + params = params.append( + 'assetSubClasses', + filtersByAssetSubClass + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + + if (filtersByTag) { + params = params.append( + 'tags', + filtersByTag + .map(({ id }) => { + return id; + }) + .join(',') + ); + } + } + + return params; + } + public createCheckoutSession({ couponId, priceId @@ -92,16 +151,6 @@ export class DataService { ); } - public fetchAdminData() { - return this.http.get('/api/v1/admin'); - } - - public fetchAdminMarketData({ filters }: { filters?: Filter[] }) { - return this.http.get('/api/v1/admin/market-data', { - params: this.buildFiltersAsQueryParams({ filters }) - }); - } - public fetchDividends({ filters, groupBy = 'month', @@ -450,65 +499,4 @@ export class DataService { couponCode }); } - - private buildFiltersAsQueryParams({ filters }: { filters?: Filter[] }) { - let params = new HttpParams(); - - if (filters?.length > 0) { - const { - ACCOUNT: filtersByAccount, - ASSET_CLASS: filtersByAssetClass, - ASSET_SUB_CLASS: filtersByAssetSubClass, - TAG: filtersByTag - } = groupBy(filters, (filter) => { - return filter.type; - }); - - if (filtersByAccount) { - params = params.append( - 'accounts', - filtersByAccount - .map(({ id }) => { - return id; - }) - .join(',') - ); - } - - if (filtersByAssetClass) { - params = params.append( - 'assetClasses', - filtersByAssetClass - .map(({ id }) => { - return id; - }) - .join(',') - ); - } - - if (filtersByAssetSubClass) { - params = params.append( - 'assetSubClasses', - filtersByAssetSubClass - .map(({ id }) => { - return id; - }) - .join(',') - ); - } - - if (filtersByTag) { - params = params.append( - 'tags', - filtersByTag - .map(({ id }) => { - return id; - }) - .join(',') - ); - } - } - - return params; - } } diff --git a/libs/common/src/lib/interfaces/admin-market-data.interface.ts b/libs/common/src/lib/interfaces/admin-market-data.interface.ts index 437b2975d..d53562a23 100644 --- a/libs/common/src/lib/interfaces/admin-market-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-market-data.interface.ts @@ -1,6 +1,7 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; export interface AdminMarketData { + count: number; marketData: AdminMarketDataItem[]; } diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 5fa80124e..703b35ea2 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -562,7 +562,6 @@