Browse Source

Feature/expose data gathering by symbol (#503)

* Expose data gathering by symbol as endpoint

* Update changelog
pull/504/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
11be6f630f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 24
      apps/api/src/app/admin/admin.controller.ts
  3. 57
      apps/api/src/services/data-gathering.service.ts
  4. 13
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html
  5. 6
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss
  6. 37
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts
  7. 3
      apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts
  8. 5
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts
  9. 37
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts
  10. 25
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html
  11. 26
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts
  12. 7
      apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss
  13. 17
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  14. 22
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  15. 9
      apps/client/src/app/components/admin-market-data/admin-market-data.module.ts
  16. 14
      apps/client/src/app/services/admin.service.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
- Exposed the data gathering by symbol as an endpoint
## 1.83.0 - 29.11.2021 ## 1.83.0 - 29.11.2021
### Changed ### Changed

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

@ -21,6 +21,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
@ -72,6 +73,29 @@ export class AdminController {
return; return;
} }
@Post('gather/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
public async gatherSymbol(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<void> {
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') @Post('gather/profile-data')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async gatherProfileData(): Promise<void> { public async gatherProfileData(): Promise<void> {

57
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[]) { public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) {
Logger.log('Profile data gathering has been started.'); Logger.log('Profile data gathering has been started.');
console.time('data-gathering-profile'); console.time('data-gathering-profile');

13
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html

@ -1,7 +1,7 @@
<div> <div class="py-2">
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex"> <div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex">
<div>{{ itemByMonth.key }}</div> <div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
<div class="align-items-center d-flex flex-grow-1 justify-content-end"> <div class="align-items-center d-flex flex-grow-1 px-1">
<div <div
*ngFor="let dayItem of days; let i = index" *ngFor="let dayItem of days; let i = index"
class="day" class="day"
@ -10,8 +10,13 @@
| date: defaultDateFormat) ?? '' | date: defaultDateFormat) ?? ''
" "
[ngClass]="{ [ngClass]="{
available: marketDataByMonth[itemByMonth.key][i + 1]?.day == i + 1 'available cursor-pointer':
marketDataByMonth[itemByMonth.key][i + 1]?.day == i + 1
}" }"
(click)="
marketDataByMonth[itemByMonth.key][i + 1] &&
onOpenMarketDataDetail(marketDataByMonth[itemByMonth.key][i + 1])
"
></div> ></div>
</div> </div>
</div> </div>

6
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss

@ -2,6 +2,12 @@
:host { :host {
display: block; display: block;
font-size: 0.9rem;
.date {
font-feature-settings: 'tnum';
font-variant-numeric: tabular-nums;
}
.day { .day {
background-color: var(--danger); background-color: var(--danger);

37
apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts

@ -5,9 +5,14 @@ import {
OnChanges, OnChanges,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { MarketData } from '@prisma/client'; import { MarketData } from '@prisma/client';
import { format } from 'date-fns'; 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({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -20,11 +25,19 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
public days = Array(31); public days = Array(31);
public defaultDateFormat = DEFAULT_DATE_FORMAT; public defaultDateFormat = DEFAULT_DATE_FORMAT;
public deviceType: string;
public marketDataByMonth: { public marketDataByMonth: {
[yearMonth: string]: { [day: string]: MarketData & { day: number } }; [yearMonth: string]: { [day: string]: MarketData & { day: number } };
} = {}; } = {};
public constructor() {} private unsubscribeSubject = new Subject<void>();
public constructor(
private deviceService: DeviceDetectorService,
private dialog: MatDialog
) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
}
public ngOnInit() {} 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();
}
} }

3
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 { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component'; import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component';
import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/market-data-detail-dialog.module';
@NgModule({ @NgModule({
declarations: [AdminMarketDataDetailComponent], declarations: [AdminMarketDataDetailComponent],
exports: [AdminMarketDataDetailComponent], exports: [AdminMarketDataDetailComponent],
imports: [CommonModule], imports: [CommonModule, GfMarketDataDetailDialogModule],
providers: [], providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })

5
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;
}

37
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<void>();
public constructor(
public dialogRef: MatDialogRef<MarketDataDetailDialog>,
@Inject(MAT_DIALOG_DATA) public data: MarketDataDetailDialogParams
) {}
public ngOnInit() {}
public onCancel(): void {
this.dialogRef.close();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

25
apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html

@ -0,0 +1,25 @@
<form class="d-flex flex-column h-100">
<h1 mat-dialog-title i18n>Details for {{ data.symbol }}</h1>
<div class="flex-grow-1" mat-dialog-content>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label>
<input matInput name="date" readonly [(ngModel)]="data.date" />
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>MarketPrice</mat-label>
<input
matInput
name="marketPrice"
readonly
[(ngModel)]="data.marketPrice"
/>
</mat-form-field>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button>
</div>
</form>

26
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 {}

7
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;
}
}

17
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -5,10 +5,11 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
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 { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; 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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -30,6 +31,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
* @constructor * @constructor
*/ */
public constructor( public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService private dataService: DataService
) {} ) {}
@ -41,6 +43,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
this.fetchAdminMarketData(); this.fetchAdminMarketData();
} }
public onGatherSymbol({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService
.gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
}
public setCurrentSymbol(aSymbol: string) { public setCurrentSymbol(aSymbol: string) {
this.marketDataDetails = []; this.marketDataDetails = [];

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

@ -8,6 +8,7 @@
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th> <th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
<th class="mat-header-cell px-1 py-2" i18n>Data Source</th> <th class="mat-header-cell px-1 py-2" i18n>Data Source</th>
<th class="mat-header-cell px-1 py-2" i18n>First Transaction</th> <th class="mat-header-cell px-1 py-2" i18n>First Transaction</th>
<th class="mat-header-cell px-1 py-2"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -22,10 +23,29 @@
<td class="mat-cell px-1 py-2"> <td class="mat-cell px-1 py-2">
{{ (item.date | date: defaultDateFormat) ?? '' }} {{ (item.date | date: defaultDateFormat) ?? '' }}
</td> </td>
<td class="mat-cell px-1 py-2">
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="accountMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #accountMenu="matMenu" xPosition="before">
<button
i18n
mat-menu-item
(click)="onGatherSymbol({dataSource: item.dataSource, symbol: item.symbol})"
>
Gather Data
</button>
</mat-menu>
</td>
</tr> </tr>
<tr *ngIf="currentSymbol === item.symbol" class="mat-row"> <tr *ngIf="currentSymbol === item.symbol" class="mat-row">
<td></td> <td></td>
<td colspan="3"> <td colspan="4">
<gf-admin-market-data-detail <gf-admin-market-data-detail
[marketData]="marketDataDetails" [marketData]="marketDataDetails"
></gf-admin-market-data-detail> ></gf-admin-market-data-detail>

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

@ -1,12 +1,19 @@
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';
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 { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
import { AdminMarketDataComponent } from './admin-market-data.component'; import { AdminMarketDataComponent } from './admin-market-data.component';
@NgModule({ @NgModule({
declarations: [AdminMarketDataComponent], declarations: [AdminMarketDataComponent],
imports: [CommonModule, GfAdminMarketDataDetailModule], imports: [
CommonModule,
GfAdminMarketDataDetailModule,
MatButtonModule,
MatMenuModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GfAdminMarketDataModule {} export class GfAdminMarketDataModule {}

14
apps/client/src/app/services/admin.service.ts

@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { DataSource } from '@prisma/client';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -14,4 +15,17 @@ export class AdminService {
public gatherProfileData() { public gatherProfileData() {
return this.http.post<void>(`/api/admin/gather/profile-data`, {}); return this.http.post<void>(`/api/admin/gather/profile-data`, {});
} }
public gatherSymbol({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.post<void>(
`/api/admin/gather/${dataSource}/${symbol}`,
{}
);
}
} }

Loading…
Cancel
Save