Feature/add market data tab to admin control panel (#497)
* Add market data tab * Update changelog
This commit is contained in:
parent
fcf07a0fd1
commit
6c07759eb7
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added tabs with routing to the admin control panel
|
- Added tabs with routing to the admin control panel
|
||||||
|
- Added a new tab to manage historical data to the admin control panel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { AdminData } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
AdminData,
|
||||||
|
AdminMarketData,
|
||||||
|
AdminMarketDataDetails
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import {
|
||||||
getPermissions,
|
getPermissions,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
@ -11,6 +15,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
|
Param,
|
||||||
Post,
|
Post,
|
||||||
UseGuards
|
UseGuards
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@ -86,4 +91,42 @@ export class AdminController {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('market-data')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getMarketData(): Promise<AdminMarketData> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.adminService.getMarketData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('market-data/:symbol')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getMarketDataBySymbol(
|
||||||
|
@Param('symbol') symbol
|
||||||
|
): Promise<AdminMarketDataDetails> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.adminService.getMarketDataBySymbol(symbol);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration.modu
|
|||||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ import { AdminService } from './admin.service';
|
|||||||
DataGatheringModule,
|
DataGatheringModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
|
MarketDataModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
SubscriptionModule
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
|
@ -2,9 +2,14 @@ import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscripti
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { baseCurrency } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { AdminData } from '@ghostfolio/common/interfaces';
|
import {
|
||||||
|
AdminData,
|
||||||
|
AdminMarketData,
|
||||||
|
AdminMarketDataDetails
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
|
||||||
@ -14,6 +19,7 @@ export class AdminService {
|
|||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly subscriptionService: SubscriptionService
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
@ -45,6 +51,31 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getMarketData(): Promise<AdminMarketData> {
|
||||||
|
return {
|
||||||
|
marketData: await (
|
||||||
|
await this.dataGatheringService.getSymbolsMax()
|
||||||
|
).map((symbol) => {
|
||||||
|
return symbol;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMarketDataBySymbol(
|
||||||
|
aSymbol: string
|
||||||
|
): Promise<AdminMarketDataDetails> {
|
||||||
|
return {
|
||||||
|
marketData: await this.marketDataService.marketDataItems({
|
||||||
|
orderBy: {
|
||||||
|
date: 'asc'
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
symbol: aSymbol
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async getLastDataGathering() {
|
private async getLastDataGathering() {
|
||||||
const lastDataGathering =
|
const lastDataGathering =
|
||||||
await this.dataGatheringService.getLastDataGathering();
|
await this.dataGatheringService.getLastDataGathering();
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { MarketDataService } from './market-data.service';
|
|
||||||
|
|
||||||
jest.mock('./market-data.service', () => {
|
jest.mock('@ghostfolio/api/services/market-data.service', () => {
|
||||||
return {
|
return {
|
||||||
MarketDataService: jest.fn().mockImplementation(() => {
|
MarketDataService: jest.fn().mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { isBefore, isToday } from 'date-fns';
|
import { isBefore, isToday } from 'date-fns';
|
||||||
@ -8,7 +9,6 @@ import { flatten } from 'lodash';
|
|||||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||||
import { GetValueParams } from './interfaces/get-value-params.interface';
|
import { GetValueParams } from './interfaces/get-value-params.interface';
|
||||||
import { GetValuesParams } from './interfaces/get-values-params.interface';
|
import { GetValuesParams } from './interfaces/get-values-params.interface';
|
||||||
import { MarketDataService } from './market-data.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CurrentRateService {
|
export class CurrentRateService {
|
||||||
|
@ -7,12 +7,12 @@ import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.mod
|
|||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation.module';
|
||||||
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CurrentRateService } from './current-rate.service';
|
import { CurrentRateService } from './current-rate.service';
|
||||||
import { MarketDataService } from './market-data.service';
|
|
||||||
import { PortfolioController } from './portfolio.controller';
|
import { PortfolioController } from './portfolio.controller';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
@ -26,6 +26,7 @@ import { RulesService } from './rules.service';
|
|||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
|
MarketDataModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
@ -35,7 +36,6 @@ import { RulesService } from './rules.service';
|
|||||||
providers: [
|
providers: [
|
||||||
AccountService,
|
AccountService,
|
||||||
CurrentRateService,
|
CurrentRateService,
|
||||||
MarketDataService,
|
|
||||||
PortfolioService,
|
PortfolioService,
|
||||||
RulesService
|
RulesService
|
||||||
]
|
]
|
||||||
|
@ -313,6 +313,52 @@ export class DataGatheringService {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
||||||
|
const startDate =
|
||||||
|
(
|
||||||
|
await this.prismaService.order.findFirst({
|
||||||
|
orderBy: [{ date: 'asc' }]
|
||||||
|
})
|
||||||
|
)?.date ?? new Date();
|
||||||
|
|
||||||
|
const currencyPairsToGather = this.exchangeRateDataService
|
||||||
|
.getCurrencyPairs()
|
||||||
|
.map(({ dataSource, symbol }) => {
|
||||||
|
return {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
date: startDate
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const symbolProfilesToGather = (
|
||||||
|
await this.prismaService.symbolProfile.findMany({
|
||||||
|
orderBy: [{ symbol: 'asc' }],
|
||||||
|
select: {
|
||||||
|
dataSource: true,
|
||||||
|
Order: {
|
||||||
|
orderBy: [{ date: 'asc' }],
|
||||||
|
select: { date: true },
|
||||||
|
take: 1
|
||||||
|
},
|
||||||
|
scraperConfiguration: true,
|
||||||
|
symbol: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map((symbolProfile) => {
|
||||||
|
return {
|
||||||
|
...symbolProfile,
|
||||||
|
date: symbolProfile.Order?.[0]?.date ?? startDate
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
...this.getBenchmarksToGather(startDate),
|
||||||
|
...currencyPairsToGather,
|
||||||
|
...symbolProfilesToGather
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public async reset() {
|
public async reset() {
|
||||||
Logger.log('Data gathering has been reset.');
|
Logger.log('Data gathering has been reset.');
|
||||||
|
|
||||||
@ -379,52 +425,6 @@ export class DataGatheringService {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
|
||||||
const startDate =
|
|
||||||
(
|
|
||||||
await this.prismaService.order.findFirst({
|
|
||||||
orderBy: [{ date: 'asc' }]
|
|
||||||
})
|
|
||||||
)?.date ?? new Date();
|
|
||||||
|
|
||||||
const currencyPairsToGather = this.exchangeRateDataService
|
|
||||||
.getCurrencyPairs()
|
|
||||||
.map(({ dataSource, symbol }) => {
|
|
||||||
return {
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
date: startDate
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const symbolProfilesToGather = (
|
|
||||||
await this.prismaService.symbolProfile.findMany({
|
|
||||||
orderBy: [{ symbol: 'asc' }],
|
|
||||||
select: {
|
|
||||||
dataSource: true,
|
|
||||||
Order: {
|
|
||||||
orderBy: [{ date: 'asc' }],
|
|
||||||
select: { date: true },
|
|
||||||
take: 1
|
|
||||||
},
|
|
||||||
scraperConfiguration: true,
|
|
||||||
symbol: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).map((symbolProfile) => {
|
|
||||||
return {
|
|
||||||
...symbolProfile,
|
|
||||||
date: symbolProfile.Order?.[0]?.date ?? startDate
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
...this.getBenchmarksToGather(startDate),
|
|
||||||
...currencyPairsToGather,
|
|
||||||
...symbolProfilesToGather
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
|
11
apps/api/src/services/market-data.module.ts
Normal file
11
apps/api/src/services/market-data.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MarketDataService } from './market-data.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [MarketDataService],
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [MarketDataService]
|
||||||
|
})
|
||||||
|
export class MarketDataModule {}
|
@ -1,9 +1,8 @@
|
|||||||
|
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { MarketData } from '@prisma/client';
|
import { MarketData, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { DateQuery } from './interfaces/date-query.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
@ -48,4 +47,22 @@ export class MarketDataService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async marketDataItems(params: {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
cursor?: Prisma.MarketDataWhereUniqueInput;
|
||||||
|
where?: Prisma.MarketDataWhereInput;
|
||||||
|
orderBy?: Prisma.MarketDataOrderByInput;
|
||||||
|
}): Promise<MarketData[]> {
|
||||||
|
const { skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
|
return this.prismaService.marketData.findMany({
|
||||||
|
cursor,
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
<div>
|
||||||
|
<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
|
||||||
|
*ngFor="let dayItem of days; let i = index"
|
||||||
|
class="day"
|
||||||
|
[title]="
|
||||||
|
(marketDataByMonth[itemByMonth.key][i + 1]?.date
|
||||||
|
| date: defaultDateFormat) ?? ''
|
||||||
|
"
|
||||||
|
[ngClass]="{
|
||||||
|
available: marketDataByMonth[itemByMonth.key][i + 1]?.day == i + 1
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.day {
|
||||||
|
background-color: var(--danger);
|
||||||
|
height: 0.5rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
width: 0.5rem;
|
||||||
|
|
||||||
|
&.available {
|
||||||
|
background-color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||||
|
import { MarketData } from '@prisma/client';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
selector: 'gf-admin-market-data-detail',
|
||||||
|
styleUrls: ['./admin-market-data-detail.component.scss'],
|
||||||
|
templateUrl: './admin-market-data-detail.component.html'
|
||||||
|
})
|
||||||
|
export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
|
||||||
|
@Input() marketData: MarketData[];
|
||||||
|
|
||||||
|
public days = Array(31);
|
||||||
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
|
public marketDataByMonth: {
|
||||||
|
[yearMonth: string]: { [day: string]: MarketData & { day: number } };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
this.marketDataByMonth = {};
|
||||||
|
|
||||||
|
for (const marketDataItem of this.marketData) {
|
||||||
|
const currentDay = parseInt(format(marketDataItem.date, 'd'), 10);
|
||||||
|
const key = format(marketDataItem.date, 'yyyy-MM');
|
||||||
|
|
||||||
|
if (!this.marketDataByMonth[key]) {
|
||||||
|
this.marketDataByMonth[key] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.marketDataByMonth[key][currentDay] = {
|
||||||
|
...marketDataItem,
|
||||||
|
day: currentDay
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AdminMarketDataDetailComponent],
|
||||||
|
exports: [AdminMarketDataDetailComponent],
|
||||||
|
imports: [CommonModule],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAdminMarketDataDetailModule {}
|
@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
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 { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
selector: 'gf-admin-market-data',
|
||||||
|
styleUrls: ['./admin-market-data.scss'],
|
||||||
|
templateUrl: './admin-market-data.html'
|
||||||
|
})
|
||||||
|
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||||
|
public currentSymbol: string;
|
||||||
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
|
public marketData: AdminMarketDataItem[] = [];
|
||||||
|
public marketDataDetails: MarketData[] = [];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.fetchAdminMarketData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCurrentSymbol(aSymbol: string) {
|
||||||
|
this.marketDataDetails = [];
|
||||||
|
|
||||||
|
if (this.currentSymbol === aSymbol) {
|
||||||
|
this.currentSymbol = '';
|
||||||
|
} else {
|
||||||
|
this.currentSymbol = aSymbol;
|
||||||
|
|
||||||
|
this.fetchAdminMarketDataBySymbol(this.currentSymbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchAdminMarketData() {
|
||||||
|
this.dataService
|
||||||
|
.fetchAdminMarketData()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ marketData }) => {
|
||||||
|
this.marketData = marketData;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchAdminMarketDataBySymbol(aSymbol: string) {
|
||||||
|
this.dataService
|
||||||
|
.fetchAdminMarketDataBySymbol(aSymbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ marketData }) => {
|
||||||
|
this.marketDataDetails = marketData;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<table class="gf-table w-100">
|
||||||
|
<thead>
|
||||||
|
<tr class="mat-header-row">
|
||||||
|
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th>
|
||||||
|
<th class="mat-header-cell px-1 py-2" i18n>Symbol</th>
|
||||||
|
<th class="mat-header-cell px-1 py-2" i18n>First transaction</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<ng-container *ngFor="let item of marketData; let i = index">
|
||||||
|
<tr
|
||||||
|
class="cursor-pointer mat-row"
|
||||||
|
(click)="setCurrentSymbol(item.symbol)"
|
||||||
|
>
|
||||||
|
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||||
|
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
||||||
|
<td class="mat-cell px-1 py-2">
|
||||||
|
{{ (item.date | date: defaultDateFormat) ?? '' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
||||||
|
<td></td>
|
||||||
|
<td colspan="2">
|
||||||
|
<gf-admin-market-data-detail
|
||||||
|
[marketData]="marketDataDetails"
|
||||||
|
></gf-admin-market-data-detail>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,12 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
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],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAdminMarketDataModule {}
|
@ -0,0 +1,5 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component';
|
||||||
import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
||||||
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
@ -13,6 +14,7 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
children: [
|
children: [
|
||||||
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
||||||
|
{ path: 'market-data', component: AdminMarketDataComponent },
|
||||||
{ path: 'overview', component: AdminOverviewComponent },
|
{ path: 'overview', component: AdminOverviewComponent },
|
||||||
{ path: 'users', component: AdminUsersComponent }
|
{ path: 'users', component: AdminUsersComponent }
|
||||||
]
|
]
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
<a
|
<a
|
||||||
*ngFor="let link of [
|
*ngFor="let link of [
|
||||||
{ iconName: 'reader-outline', path: 'overview' },
|
{ iconName: 'reader-outline', path: 'overview' },
|
||||||
{ iconName: 'people-outline', path: 'users' }
|
{ iconName: 'people-outline', path: 'users' },
|
||||||
|
{ iconName: 'server-outline', path: 'market-data' }
|
||||||
]"
|
]"
|
||||||
#rla="routerLinkActive"
|
#rla="routerLinkActive"
|
||||||
mat-tab-link
|
mat-tab-link
|
||||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
|
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
||||||
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
||||||
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
@ -18,6 +19,7 @@ import { AdminPageComponent } from './admin-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
AdminPageRoutingModule,
|
AdminPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfAdminMarketDataModule,
|
||||||
GfAdminOverviewModule,
|
GfAdminOverviewModule,
|
||||||
GfAdminUsersModule,
|
GfAdminUsersModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
padding-bottom: constant(safe-area-inset-bottom);
|
padding-bottom: constant(safe-area-inset-bottom);
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
|
gf-admin-market-data,
|
||||||
gf-admin-overview,
|
gf-admin-overview,
|
||||||
gf-admin-users {
|
gf-admin-users {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -16,6 +16,8 @@ import {
|
|||||||
Access,
|
Access,
|
||||||
Accounts,
|
Accounts,
|
||||||
AdminData,
|
AdminData,
|
||||||
|
AdminMarketData,
|
||||||
|
AdminMarketDataDetails,
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
@ -64,6 +66,23 @@ export class DataService {
|
|||||||
return this.http.get<AdminData>('/api/admin');
|
return this.http.get<AdminData>('/api/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchAdminMarketData() {
|
||||||
|
return this.http.get<AdminMarketData>('/api/admin/market-data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchAdminMarketDataBySymbol(
|
||||||
|
aSymbol: string
|
||||||
|
): Observable<AdminMarketDataDetails> {
|
||||||
|
return this.http.get<any>(`/api/admin/market-data/${aSymbol}`).pipe(
|
||||||
|
map((data) => {
|
||||||
|
for (const item of data.marketData) {
|
||||||
|
item.date = parseISO(item.date);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public deleteAccess(aId: string) {
|
public deleteAccess(aId: string) {
|
||||||
return this.http.delete<any>(`/api/access/${aId}`);
|
return this.http.delete<any>(`/api/access/${aId}`);
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,10 @@ ngx-skeleton-loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.gf-table {
|
.gf-table {
|
||||||
@include gf-table;
|
@include gf-table;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { MarketData } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface AdminMarketDataDetails {
|
||||||
|
marketData: MarketData[];
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
export interface AdminMarketData {
|
||||||
|
marketData: AdminMarketDataItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminMarketDataItem {
|
||||||
|
symbol: string;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { Access } from './access.interface';
|
import { Access } from './access.interface';
|
||||||
import { Accounts } from './accounts.interface';
|
import { Accounts } from './accounts.interface';
|
||||||
import { AdminData } from './admin-data.interface';
|
import { AdminData } from './admin-data.interface';
|
||||||
|
import { AdminMarketDataDetails } from './admin-market-data-details.interface';
|
||||||
|
import { AdminMarketData } from './admin-market-data.interface';
|
||||||
import { Export } from './export.interface';
|
import { Export } from './export.interface';
|
||||||
import { InfoItem } from './info-item.interface';
|
import { InfoItem } from './info-item.interface';
|
||||||
import { PortfolioChart } from './portfolio-chart.interface';
|
import { PortfolioChart } from './portfolio-chart.interface';
|
||||||
@ -23,6 +25,8 @@ export {
|
|||||||
Access,
|
Access,
|
||||||
Accounts,
|
Accounts,
|
||||||
AdminData,
|
AdminData,
|
||||||
|
AdminMarketData,
|
||||||
|
AdminMarketDataDetails,
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user