Feature/expose data gathering by symbol (#503)
* Expose data gathering by symbol as endpoint * Update changelog
This commit is contained in:
parent
85d123e1b1
commit
11be6f630f
@ -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 {}
|
@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: unset;
|
||||
}
|
||||
}
|
@ -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…
x
Reference in New Issue
Block a user