Feature/extend data providers management of admin control panel (#4615)
* Extend data providers management * Update changelog
This commit is contained in:
parent
1b5a65d391
commit
b90bfc3d6e
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
|
||||
- Changed the column label from _Index_ to _Name_ in the benchmark component
|
||||
- Extended the data providers management of the admin control panel
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
## 2.156.0 - 2025-04-27
|
||||
|
@ -68,7 +68,7 @@ export class AdminController {
|
||||
@HasPermission(permissions.accessAdminControl)
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
public async getAdminData(): Promise<AdminData> {
|
||||
return this.adminService.get();
|
||||
return this.adminService.get({ user: this.request.user });
|
||||
}
|
||||
|
||||
@HasPermission(permissions.accessAdminControl)
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
Filter
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
import { MarketDataPreset } from '@ghostfolio/common/types';
|
||||
import { MarketDataPreset, UserWithSettings } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
@ -134,7 +134,9 @@ export class AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
public async get(): Promise<AdminData> {
|
||||
public async get({ user }: { user: UserWithSettings }): Promise<AdminData> {
|
||||
const dataSources = await this.dataProviderService.getDataSources({ user });
|
||||
|
||||
const [settings, transactionCount, userCount] = await Promise.all([
|
||||
this.propertyService.get(),
|
||||
this.prismaService.order.count(),
|
||||
@ -145,6 +147,11 @@ export class AdminService {
|
||||
settings,
|
||||
transactionCount,
|
||||
userCount,
|
||||
dataProviders: dataSources.map((dataSource) => {
|
||||
return this.dataProviderService
|
||||
.getDataProvider(dataSource)
|
||||
.getDataProviderInfo();
|
||||
}),
|
||||
version: environment.version
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service';
|
||||
import {
|
||||
GetAssetProfileParams,
|
||||
GetDividendsParams,
|
||||
@ -327,10 +328,15 @@ export class GhostfolioService {
|
||||
}
|
||||
|
||||
private getDataProviderInfo(): DataProviderInfo {
|
||||
const ghostfolioDataProviderService = new GhostfolioDataProviderService(
|
||||
this.configurationService,
|
||||
this.propertyService
|
||||
);
|
||||
|
||||
return {
|
||||
...ghostfolioDataProviderService.getDataProviderInfo(),
|
||||
isPremium: false,
|
||||
name: 'Ghostfolio Premium',
|
||||
url: 'https://ghostfol.io'
|
||||
name: 'Ghostfolio Premium'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.ALPHA_VANTAGE,
|
||||
isPremium: false,
|
||||
name: 'Alpha Vantage',
|
||||
url: 'https://www.alphavantage.co'
|
||||
|
@ -92,6 +92,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.COINGECKO,
|
||||
isPremium: false,
|
||||
name: 'CoinGecko',
|
||||
url: 'https://coingecko.com'
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
LookupItem,
|
||||
LookupResponse
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasRole } from '@ghostfolio/common/permissions';
|
||||
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
|
||||
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
@ -169,6 +170,7 @@ export class DataProviderService {
|
||||
let dataSourcesKey: 'DATA_SOURCES' | 'DATA_SOURCES_LEGACY' = 'DATA_SOURCES';
|
||||
|
||||
if (
|
||||
!hasRole(user, 'ADMIN') &&
|
||||
isBefore(user.createdAt, new Date('2025-03-23')) &&
|
||||
this.configurationService.get('DATA_SOURCES_LEGACY')?.length > 0
|
||||
) {
|
||||
@ -185,7 +187,7 @@ export class DataProviderService {
|
||||
PROPERTY_API_KEY_GHOSTFOLIO
|
||||
)) as string;
|
||||
|
||||
if (ghostfolioApiKey) {
|
||||
if (ghostfolioApiKey || hasRole(user, 'ADMIN')) {
|
||||
dataSources.push('GHOSTFOLIO');
|
||||
}
|
||||
|
||||
@ -670,6 +672,7 @@ export class DataProviderService {
|
||||
lookupItem.dataProviderInfo.isPremium = false;
|
||||
}
|
||||
|
||||
lookupItem.dataProviderInfo.dataSource = undefined;
|
||||
lookupItem.dataProviderInfo.name = undefined;
|
||||
lookupItem.dataProviderInfo.url = undefined;
|
||||
} else {
|
||||
|
@ -68,6 +68,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
isPremium: true,
|
||||
name: 'EOD Historical Data',
|
||||
url: 'https://eodhd.com'
|
||||
|
@ -223,6 +223,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||
isPremium: true,
|
||||
name: 'Financial Modeling Prep',
|
||||
url: 'https://financialmodelingprep.com/developer/docs'
|
||||
|
@ -92,9 +92,10 @@ export class GhostfolioService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.GHOSTFOLIO,
|
||||
isPremium: true,
|
||||
name: 'Ghostfolio',
|
||||
url: 'https://ghostfo.io'
|
||||
url: 'https://ghostfol.io'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,7 @@ export class GoogleSheetsService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.GOOGLE_SHEETS,
|
||||
isPremium: false,
|
||||
name: 'Google Sheets',
|
||||
url: 'https://docs.google.com/spreadsheets'
|
||||
|
@ -64,6 +64,7 @@ export class ManualService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.MANUAL,
|
||||
isPremium: false
|
||||
};
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ export class RapidApiService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.RAPID_API,
|
||||
isPremium: false,
|
||||
name: 'Rapid API',
|
||||
url: 'https://rapidapi.com'
|
||||
|
@ -51,6 +51,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
|
||||
public getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
dataSource: DataSource.YAHOO,
|
||||
isPremium: false,
|
||||
name: 'Yahoo Finance',
|
||||
url: 'https://finance.yahoo.com'
|
||||
|
@ -4,74 +4,100 @@
|
||||
<h2 class="text-center" i18n>Data Providers</h2>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<div class="align-items-center d-flex my-3">
|
||||
<div class="w-50">
|
||||
<a
|
||||
class="align-items-center d-inline-flex"
|
||||
target="_blank"
|
||||
[href]="pricingUrl"
|
||||
>
|
||||
@if (isGhostfolioApiKeyValid === false) {
|
||||
<span class="badge badge-warning mr-1" i18n
|
||||
>Early Access</span
|
||||
>
|
||||
}
|
||||
Ghostfolio Premium
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
/>
|
||||
</a>
|
||||
@if (isGhostfolioApiKeyValid === true) {
|
||||
<div class="line-height-1">
|
||||
<small class="text-muted">
|
||||
<ng-container i18n>Valid until</ng-container>
|
||||
{{
|
||||
ghostfolioApiStatus?.subscription?.expiresAt
|
||||
| date: defaultDateFormat
|
||||
}}</small
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="w-50">
|
||||
@if (isGhostfolioApiKeyValid === true) {
|
||||
<div class="align-items-center d-flex flex-wrap">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
{{ ghostfolioApiStatus.dailyRequests }}
|
||||
<ng-container i18n>of</ng-container>
|
||||
{{ ghostfolioApiStatus.dailyRequestsMax }}
|
||||
<ng-container i18n>daily requests</ng-container>
|
||||
@for (dataProvider of dataProviders; track dataProvider.name) {
|
||||
<div class="align-items-center d-flex my-3">
|
||||
@if (dataProvider.name === 'Ghostfolio') {
|
||||
<div class="w-50">
|
||||
<div class="d-flex">
|
||||
<gf-asset-profile-icon
|
||||
class="mr-1"
|
||||
[url]="dataProvider.url"
|
||||
/>
|
||||
<div>
|
||||
<a
|
||||
class="align-items-center d-inline-flex"
|
||||
target="_blank"
|
||||
[href]="pricingUrl"
|
||||
>
|
||||
Ghostfolio Premium
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
/>
|
||||
@if (isGhostfolioApiKeyValid === false) {
|
||||
<span class="badge badge-warning ml-2" i18n
|
||||
>Early Access</span
|
||||
>
|
||||
}
|
||||
</a>
|
||||
@if (isGhostfolioApiKeyValid === true) {
|
||||
<div class="line-height-1">
|
||||
<small class="text-muted">
|
||||
<ng-container i18n>Valid until</ng-container>
|
||||
{{
|
||||
ghostfolioApiStatus?.subscription?.expiresAt
|
||||
| date: defaultDateFormat
|
||||
}}</small
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="ghostfolioApiMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #ghostfolioApiMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onRemoveGhostfolioApiKey()">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Remove API key</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
} @else if (isGhostfolioApiKeyValid === false) {
|
||||
<button
|
||||
color="accent"
|
||||
mat-flat-button
|
||||
(click)="onSetGhostfolioApiKey()"
|
||||
>
|
||||
<ion-icon class="mr-1" name="key-outline" />
|
||||
<span i18n>Set API key</span>
|
||||
</button>
|
||||
<div class="w-50">
|
||||
@if (isGhostfolioApiKeyValid === true) {
|
||||
<div class="align-items-center d-flex flex-wrap">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
{{ ghostfolioApiStatus.dailyRequests }}
|
||||
<ng-container i18n>of</ng-container>
|
||||
{{ ghostfolioApiStatus.dailyRequestsMax }}
|
||||
<ng-container i18n>daily requests</ng-container>
|
||||
</div>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="ghostfolioApiMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #ghostfolioApiMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onRemoveGhostfolioApiKey()"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Remove API key</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
} @else if (isGhostfolioApiKeyValid === false) {
|
||||
<button
|
||||
color="accent"
|
||||
mat-flat-button
|
||||
(click)="onSetGhostfolioApiKey()"
|
||||
>
|
||||
<ion-icon class="mr-1" name="key-outline" />
|
||||
<span i18n>Set API key</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="w-50">
|
||||
<div class="d-flex">
|
||||
<gf-asset-profile-icon
|
||||
class="mr-1"
|
||||
[url]="dataProvider.url"
|
||||
/>
|
||||
{{ dataProvider.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-50"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderGhostfolioStatusResponse,
|
||||
DataProviderInfo,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
@ -35,6 +36,7 @@ import { GhostfolioPremiumApiDialogParams } from './ghostfolio-premium-api-dialo
|
||||
standalone: false
|
||||
})
|
||||
export class AdminSettingsComponent implements OnDestroy, OnInit {
|
||||
public dataProviders: DataProviderInfo[];
|
||||
public defaultDateFormat: string;
|
||||
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
|
||||
public isGhostfolioApiKeyValid: boolean;
|
||||
@ -124,23 +126,36 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
|
||||
|
||||
private initialize() {
|
||||
this.adminService
|
||||
.fetchGhostfolioDataProviderStatus()
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
this.isGhostfolioApiKeyValid = false;
|
||||
.fetchAdminData()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ dataProviders, settings }) => {
|
||||
this.dataProviders = dataProviders.filter(({ dataSource }) => {
|
||||
return dataSource !== 'MANUAL';
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
this.adminService
|
||||
.fetchGhostfolioDataProviderStatus(
|
||||
settings[PROPERTY_API_KEY_GHOSTFOLIO] as string
|
||||
)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
this.isGhostfolioApiKeyValid = false;
|
||||
|
||||
return of(null);
|
||||
}),
|
||||
filter((status) => {
|
||||
return status !== null;
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe((status) => {
|
||||
this.ghostfolioApiStatus = status;
|
||||
this.isGhostfolioApiKeyValid = true;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
return of(null);
|
||||
}),
|
||||
filter((status) => {
|
||||
return status !== null;
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe((status) => {
|
||||
this.ghostfolioApiStatus = status;
|
||||
this.isGhostfolioApiKeyValid = true;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
||||
import { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.module';
|
||||
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
@ -17,6 +18,7 @@ import { AdminSettingsComponent } from './admin-settings.component';
|
||||
CommonModule,
|
||||
GfAdminPlatformModule,
|
||||
GfAdminTagModule,
|
||||
GfAssetProfileIconComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
|
@ -4,8 +4,7 @@ import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.
|
||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import {
|
||||
HEADER_KEY_SKIP_INTERCEPTOR,
|
||||
HEADER_KEY_TOKEN,
|
||||
PROPERTY_API_KEY_GHOSTFOLIO
|
||||
HEADER_KEY_TOKEN
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||
import {
|
||||
@ -24,7 +23,6 @@ import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { DataSource, MarketData, Platform } from '@prisma/client';
|
||||
import { JobStatus } from 'bull';
|
||||
import { switchMap } from 'rxjs';
|
||||
|
||||
import { environment } from '../../environments/environment';
|
||||
import { DataService } from './data.service';
|
||||
@ -115,19 +113,15 @@ export class AdminService {
|
||||
});
|
||||
}
|
||||
|
||||
public fetchGhostfolioDataProviderStatus() {
|
||||
return this.fetchAdminData().pipe(
|
||||
switchMap(({ settings }) => {
|
||||
const headers = new HttpHeaders({
|
||||
[HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
|
||||
[HEADER_KEY_TOKEN]: `Api-Key ${settings[PROPERTY_API_KEY_GHOSTFOLIO]}`
|
||||
});
|
||||
public fetchGhostfolioDataProviderStatus(aApiKey: string) {
|
||||
const headers = new HttpHeaders({
|
||||
[HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
|
||||
[HEADER_KEY_TOKEN]: `Api-Key ${aApiKey}`
|
||||
});
|
||||
|
||||
return this.http.get<DataProviderGhostfolioStatusResponse>(
|
||||
`${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`,
|
||||
{ headers }
|
||||
);
|
||||
})
|
||||
return this.http.get<DataProviderGhostfolioStatusResponse>(
|
||||
`${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`,
|
||||
{ headers }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { DataProviderInfo } from './data-provider-info.interface';
|
||||
|
||||
export interface AdminData {
|
||||
dataProviders: DataProviderInfo[];
|
||||
settings: { [key: string]: boolean | object | string | string[] };
|
||||
transactionCount: number;
|
||||
userCount: number;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { DataSource } from '@prisma/client';
|
||||
|
||||
export interface DataProviderInfo {
|
||||
dataSource?: DataSource;
|
||||
isPremium: boolean;
|
||||
name?: string;
|
||||
url?: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user