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 85d123e1b1
commit 11be6f630f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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<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')
@UseGuards(AuthGuard('jwt'))
public async gatherProfileData(): Promise<void> {

@ -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');

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

@ -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);

@ -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<void>();
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();
}
}

@ -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]
})

@ -0,0 +1,5 @@
export interface MarketDataDetailDialogParams {
date: string;
marketPrice: number;
symbol: string;
}

@ -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();
}
}

@ -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>

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

@ -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 = [];

@ -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>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"></th>
</tr>
</thead>
<tbody>
@ -22,10 +23,29 @@
<td class="mat-cell px-1 py-2">
{{ (item.date | date: defaultDateFormat) ?? '' }}
</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 *ngIf="currentSymbol === item.symbol" class="mat-row">
<td></td>
<td colspan="3">
<td colspan="4">
<gf-admin-market-data-detail
[marketData]="marketDataDetails"
></gf-admin-market-data-detail>

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

@ -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<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