From 11be6f630fd41f469905bfe33a97e6fce29a2c71 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 30 Nov 2021 21:06:10 +0100 Subject: [PATCH] Feature/expose data gathering by symbol (#503) * Expose data gathering by symbol as endpoint * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/admin/admin.controller.ts | 24 ++++++++ .../src/services/data-gathering.service.ts | 57 +++++++++++++++++++ .../admin-market-data-detail.component.html | 13 +++-- .../admin-market-data-detail.component.scss | 6 ++ .../admin-market-data-detail.component.ts | 37 +++++++++++- .../admin-market-data-detail.module.ts | 3 +- .../interfaces/interfaces.ts | 5 ++ .../market-data-detail-dialog.component.ts | 37 ++++++++++++ .../market-data-detail-dialog.html | 25 ++++++++ .../market-data-detail-dialog.module.ts | 26 +++++++++ .../market-data-detail-dialog.scss | 7 +++ .../admin-market-data.component.ts | 17 +++++- .../admin-market-data/admin-market-data.html | 22 ++++++- .../admin-market-data.module.ts | 9 ++- apps/client/src/app/services/admin.service.ts | 14 +++++ 16 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts create mode 100644 apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts create mode 100644 apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html create mode 100644 apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts create mode 100644 apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index cb87b3fd..b4b49511 100644 --- a/CHANGELOG.md +++ b/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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Exposed the data gathering by symbol as an endpoint + ## 1.83.0 - 29.11.2021 ### Changed diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index 3ace4126..4788a02c 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -21,6 +21,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { DataSource } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AdminService } from './admin.service'; @@ -72,6 +73,29 @@ export class AdminController { return; } + @Post('gather/:dataSource/:symbol') + @UseGuards(AuthGuard('jwt')) + public async gatherSymbol( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + if ( + !hasPermission( + getPermissions(this.request.user.role), + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + this.dataGatheringService.gatherSymbol({ dataSource, symbol }); + + return; + } + @Post('gather/profile-data') @UseGuards(AuthGuard('jwt')) public async gatherProfileData(): Promise { diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 18a95b16..fb2f3422 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -120,6 +120,63 @@ export class DataGatheringService { } } + public async gatherSymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + const isDataGatheringLocked = await this.prismaService.property.findUnique({ + where: { key: 'LOCKED_DATA_GATHERING' } + }); + + if (!isDataGatheringLocked) { + Logger.log(`Symbol data gathering for ${symbol} has been started.`); + console.time('data-gathering-symbol'); + + await this.prismaService.property.create({ + data: { + key: 'LOCKED_DATA_GATHERING', + value: new Date().toISOString() + } + }); + + const symbols = (await this.getSymbolsMax()).filter( + (dataGatheringItem) => { + return ( + dataGatheringItem.dataSource === dataSource && + dataGatheringItem.symbol === symbol + ); + } + ); + + try { + await this.gatherSymbols(symbols); + + await this.prismaService.property.upsert({ + create: { + key: 'LAST_DATA_GATHERING', + value: new Date().toISOString() + }, + update: { value: new Date().toISOString() }, + where: { key: 'LAST_DATA_GATHERING' } + }); + } catch (error) { + Logger.error(error); + } + + await this.prismaService.property.delete({ + where: { + key: 'LOCKED_DATA_GATHERING' + } + }); + + Logger.log(`Symbol data gathering for ${symbol} has been completed.`); + console.timeEnd('data-gathering-symbol'); + } + } + public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) { Logger.log('Profile data gathering has been started.'); console.time('data-gathering-profile'); diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html index 433cb0df..66c3388d 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html @@ -1,7 +1,7 @@ -
+
-
{{ itemByMonth.key }}
-
+
{{ itemByMonth.key }}
+
diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss index 12c57e20..d16aec1d 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss @@ -2,6 +2,12 @@ :host { display: block; + font-size: 0.9rem; + + .date { + font-feature-settings: 'tnum'; + font-variant-numeric: tabular-nums; + } .day { background-color: var(--danger); diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts index ed1b0874..8c297a36 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts @@ -5,9 +5,14 @@ import { OnChanges, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { MarketData } from '@prisma/client'; import { format } from 'date-fns'; +import { DeviceDetectorService } from 'ngx-device-detector'; +import { Subject, takeUntil } from 'rxjs'; + +import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data-detail-dialog.component'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -20,11 +25,19 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { public days = Array(31); public defaultDateFormat = DEFAULT_DATE_FORMAT; + public deviceType: string; public marketDataByMonth: { [yearMonth: string]: { [day: string]: MarketData & { day: number } }; } = {}; - public constructor() {} + private unsubscribeSubject = new Subject(); + + public constructor( + private deviceService: DeviceDetectorService, + private dialog: MatDialog + ) { + this.deviceType = this.deviceService.getDeviceInfo().deviceType; + } public ngOnInit() {} @@ -45,4 +58,26 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit { }; } } + + public onOpenMarketDataDetail({ date, marketPrice, symbol }: MarketData) { + const dialogRef = this.dialog.open(MarketDataDetailDialog, { + data: { + marketPrice, + symbol, + date: format(date, DEFAULT_DATE_FORMAT) + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } } diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts index b5b3310f..9ea09ab5 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts @@ -2,11 +2,12 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component'; +import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/market-data-detail-dialog.module'; @NgModule({ declarations: [AdminMarketDataDetailComponent], exports: [AdminMarketDataDetailComponent], - imports: [CommonModule], + imports: [CommonModule, GfMarketDataDetailDialogModule], providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts new file mode 100644 index 00000000..a7defb81 --- /dev/null +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +export interface MarketDataDetailDialogParams { + date: string; + marketPrice: number; + symbol: string; +} diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts new file mode 100644 index 00000000..4ad1ebaa --- /dev/null +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts @@ -0,0 +1,37 @@ +import { + ChangeDetectionStrategy, + Component, + Inject, + OnDestroy +} from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; + +import { MarketDataDetailDialogParams } from './interfaces/interfaces'; + +@Component({ + host: { class: 'h-100' }, + selector: 'gf-market-data-detail-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./market-data-detail-dialog.scss'], + templateUrl: 'market-data-detail-dialog.html' +}) +export class MarketDataDetailDialog implements OnDestroy { + private unsubscribeSubject = new Subject(); + + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: MarketDataDetailDialogParams + ) {} + + public ngOnInit() {} + + public onCancel(): void { + this.dialogRef.close(); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } +} diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html new file mode 100644 index 00000000..65b3578f --- /dev/null +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html @@ -0,0 +1,25 @@ +
+

Details for {{ data.symbol }}

+
+
+ + Date + + +
+
+ + MarketPrice + + +
+
+
+ +
+
diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts new file mode 100644 index 00000000..9a7235c8 --- /dev/null +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +import { MarketDataDetailDialog } from './market-data-detail-dialog.component'; + +@NgModule({ + declarations: [MarketDataDetailDialog], + exports: [], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule + ], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfMarketDataDetailDialogModule {} diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss new file mode 100644 index 00000000..ce1c7d59 --- /dev/null +++ b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss @@ -0,0 +1,7 @@ +:host { + display: block; + + .mat-dialog-content { + max-height: unset; + } +} 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 2492d88e..256a86f5 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 @@ -5,10 +5,11 @@ import { OnDestroy, OnInit } from '@angular/core'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; -import { MarketData } from '@prisma/client'; +import { DataSource, MarketData } from '@prisma/client'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -30,6 +31,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { * @constructor */ public constructor( + private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, private dataService: DataService ) {} @@ -41,6 +43,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.fetchAdminMarketData(); } + public onGatherSymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.adminService + .gatherSymbol({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + public setCurrentSymbol(aSymbol: string) { this.marketDataDetails = []; 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 7812dbbe..cadcc785 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 @@ -8,6 +8,7 @@ Symbol Data Source First Transaction + @@ -22,10 +23,29 @@ {{ (item.date | date: defaultDateFormat) ?? '' }} + + + + + + - + 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 b3996d70..1a3432ff 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 @@ -1,12 +1,19 @@ 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 { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; import { AdminMarketDataComponent } from './admin-market-data.component'; @NgModule({ declarations: [AdminMarketDataComponent], - imports: [CommonModule, GfAdminMarketDataDetailModule], + imports: [ + CommonModule, + GfAdminMarketDataDetailModule, + MatButtonModule, + MatMenuModule + ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class GfAdminMarketDataModule {} diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 92516f5d..53d76af7 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { DataSource } from '@prisma/client'; @Injectable({ providedIn: 'root' @@ -14,4 +15,17 @@ export class AdminService { public gatherProfileData() { return this.http.post(`/api/admin/gather/profile-data`, {}); } + + public gatherSymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + return this.http.post( + `/api/admin/gather/${dataSource}/${symbol}`, + {} + ); + } }