Browse Source

Add asset sub class filter

pull/1188/head
Thomas 3 years ago
parent
commit
73c5ff37f3
  1. 21
      apps/api/src/app/admin/admin.controller.ts
  2. 27
      apps/api/src/app/admin/admin.service.ts
  3. 68
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  4. 10
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  5. 2
      apps/client/src/app/components/admin-market-data/admin-market-data.module.ts
  6. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts
  7. 28
      apps/client/src/app/services/data.service.ts
  8. 2
      libs/common/src/lib/interfaces/filter.interface.ts

21
apps/api/src/app/admin/admin.controller.ts

@ -8,7 +8,8 @@ import {
import { import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails AdminMarketDataDetails,
Filter
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -22,6 +23,7 @@ import {
Param, Param,
Post, Post,
Put, Put,
Query,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
@ -226,7 +228,9 @@ export class AdminController {
@Get('market-data') @Get('market-data')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getMarketData(): Promise<AdminMarketData> { public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string
): Promise<AdminMarketData> {
if ( if (
!hasPermission( !hasPermission(
this.request.user.permissions, 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 <Filter>{
id: assetSubClass,
type: 'ASSET_SUB_CLASS'
};
})
];
return this.adminService.getMarketData(filters);
} }
@Get('market-data/:dataSource/:symbol') @Get('market-data/:dataSource/:symbol')

27
apps/api/src/app/admin/admin.service.ts

@ -11,11 +11,13 @@ import {
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem, AdminMarketDataItem,
Filter,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Property } from '@prisma/client'; import { AssetSubClass, Prisma, Property } from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { groupBy } from 'lodash';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
@ -63,14 +65,27 @@ export class AdminService {
}; };
} }
public async getMarketData(): Promise<AdminMarketData> { public async getMarketData(filters?: Filter[]): Promise<AdminMarketData> {
const where: Prisma.SymbolProfileWhereInput = {};
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy(
filters,
(filter) => {
return filter.type;
}
);
const marketData = await this.prismaService.marketData.groupBy({ const marketData = await this.prismaService.marketData.groupBy({
_count: true, _count: true,
by: ['dataSource', 'symbol'] by: ['dataSource', 'symbol']
}); });
const currencyPairsToGather: AdminMarketDataItem[] = let currencyPairsToGather: AdminMarketDataItem[] = [];
this.exchangeRateDataService
if (filtersByAssetSubClass) {
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
} else {
currencyPairsToGather = this.exchangeRateDataService
.getCurrencyPairs() .getCurrencyPairs()
.map(({ dataSource, symbol }) => { .map(({ dataSource, symbol }) => {
const marketDataItemCount = const marketDataItemCount =
@ -89,9 +104,11 @@ export class AdminService {
sectorsCount: 0 sectorsCount: 0
}; };
}); });
}
const symbolProfilesToGather: AdminMarketDataItem[] = ( const symbolProfilesToGather: AdminMarketDataItem[] = (
await this.prismaService.symbolProfile.findMany({ await this.prismaService.symbolProfile.findMany({
where,
orderBy: [{ symbol: 'asc' }], orderBy: [{ symbol: 'asc' }],
select: { select: {
_count: { _count: {
@ -100,7 +117,6 @@ export class AdminService {
assetClass: true, assetClass: true,
assetSubClass: true, assetSubClass: true,
countries: true, countries: true,
sectors: true,
dataSource: true, dataSource: true,
Order: { Order: {
orderBy: [{ date: 'asc' }], orderBy: [{ date: 'asc' }],
@ -108,6 +124,7 @@ export class AdminService {
take: 1 take: 1
}, },
scraperConfiguration: true, scraperConfiguration: true,
sectors: true,
symbol: true symbol: true
} }
}) })

68
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 { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DATE_FORMAT, getDateFormatString } from '@ghostfolio/common/helper'; 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 { 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 { format, parseISO } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; 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 { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component';
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces'; 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 { export class AdminMarketDataComponent implements OnDestroy, OnInit {
@ViewChild(MatSort) sort: MatSort; @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 currentDataSource: DataSource;
public currentSymbol: string; public currentSymbol: string;
public dataSource: MatTableDataSource<any> = new MatTableDataSource(); public dataSource: MatTableDataSource<AdminMarketDataItem> =
new MatTableDataSource();
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string; public deviceType: string;
public displayedColumns = [ public displayedColumns = [
@ -50,7 +69,9 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
'sectorsCount', 'sectorsCount',
'actions' 'actions'
]; ];
public marketData: AdminMarketDataItem[] = []; public filters$ = new Subject<Filter[]>();
public isLoading = false;
public placeholder = '';
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -98,7 +119,29 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; 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) { public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
@ -142,19 +185,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); 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({ private openAssetProfileDialog({
dataSource, dataSource,
dateOfFirstActivity, dateOfFirstActivity,

10
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -1,4 +1,14 @@
<div class="container"> <div class="container">
<div class="row">
<div class="col">
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoading"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
></gf-activities-filter>
</div>
</div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table <table

2
apps/client/src/app/components/admin-market-data/admin-market-data.module.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { AdminMarketDataComponent } from './admin-market-data.component'; import { AdminMarketDataComponent } from './admin-market-data.component';
import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profile-dialog.module'; import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profile-dialog.module';
@ -12,6 +13,7 @@ import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profil
declarations: [AdminMarketDataComponent], declarations: [AdminMarketDataComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesFilterModule,
GfAssetProfileDialogModule, GfAssetProfileDialogModule,
MatButtonModule, MatButtonModule,
MatMenuModule, MatMenuModule,

2
apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts

@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfAdminMarketDataDetailModule } from '../../admin-market-data-detail/admin-market-data-detail.module';
import { AssetProfileDialog } from './asset-profile-dialog.component'; import { AssetProfileDialog } from './asset-profile-dialog.component';

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

@ -133,8 +133,32 @@ export class DataService {
return this.http.get<AdminData>('/api/v1/admin'); return this.http.get<AdminData>('/api/v1/admin');
} }
public fetchAdminMarketData() { public fetchAdminMarketData({ filters }: { filters?: Filter[] }) {
return this.http.get<AdminMarketData>('/api/v1/admin/market-data'); 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<AdminMarketData>('/api/v1/admin/market-data', {
params
});
} }
public deleteAccess(aId: string) { public deleteAccess(aId: string) {

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

@ -1,5 +1,5 @@
export interface Filter { export interface Filter {
id: string; id: string;
label?: string; label?: string;
type: 'ACCOUNT' | 'ASSET_CLASS' | 'SYMBOL' | 'TAG'; type: 'ACCOUNT' | 'ASSET_CLASS' | 'ASSET_SUB_CLASS' | 'SYMBOL' | 'TAG';
} }

Loading…
Cancel
Save