Browse Source

Extend data providers management

pull/4615/head
Thomas Kaul 4 months ago
parent
commit
c1fb111b73
  1. 14
      apps/api/src/app/admin/admin.service.ts
  2. 1
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  3. 1
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  4. 1
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  5. 1
      apps/api/src/services/data-provider/data-provider.service.ts
  6. 1
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  7. 1
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  8. 3
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  9. 1
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  10. 1
      apps/api/src/services/data-provider/manual/manual.service.ts
  11. 1
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  12. 1
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  13. 152
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  14. 49
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  15. 2
      apps/client/src/app/components/admin-settings/admin-settings.module.ts
  16. 26
      apps/client/src/app/services/admin.service.ts
  17. 3
      libs/common/src/lib/interfaces/admin-data.interface.ts
  18. 3
      libs/common/src/lib/interfaces/data-provider-info.interface.ts

14
apps/api/src/app/admin/admin.service.ts

@ -25,6 +25,7 @@ import {
AdminMarketDataItem, AdminMarketDataItem,
AdminUsers, AdminUsers,
AssetProfileIdentifier, AssetProfileIdentifier,
DataProviderInfo,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter Filter
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -145,6 +146,7 @@ export class AdminService {
settings, settings,
transactionCount, transactionCount,
userCount, userCount,
dataProviders: this.getDataProviders(),
version: environment.version version: environment.version
}; };
} }
@ -633,6 +635,18 @@ export class AdminService {
}); });
} }
private getDataProviders(): DataProviderInfo[] {
return this.configurationService
.get('DATA_SOURCES')
.concat('GHOSTFOLIO')
.sort()
.map((dataSource) => {
return this.dataProviderService
.getDataProvider(DataSource[dataSource])
.getDataProviderInfo();
});
}
private getExtendedPrismaClient() { private getExtendedPrismaClient() {
Logger.debug('Connect extended prisma client', 'AdminService'); Logger.debug('Connect extended prisma client', 'AdminService');

1
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -328,6 +328,7 @@ export class GhostfolioService {
private getDataProviderInfo(): DataProviderInfo { private getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.GHOSTFOLIO,
isPremium: false, isPremium: false,
name: 'Ghostfolio Premium', name: 'Ghostfolio Premium',
url: 'https://ghostfol.io' url: 'https://ghostfol.io'

1
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -52,6 +52,7 @@ export class AlphaVantageService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.ALPHA_VANTAGE,
isPremium: false, isPremium: false,
name: 'Alpha Vantage', name: 'Alpha Vantage',
url: 'https://www.alphavantage.co' url: 'https://www.alphavantage.co'

1
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -92,6 +92,7 @@ export class CoinGeckoService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.COINGECKO,
isPremium: false, isPremium: false,
name: 'CoinGecko', name: 'CoinGecko',
url: 'https://coingecko.com' url: 'https://coingecko.com'

1
apps/api/src/services/data-provider/data-provider.service.ts

@ -670,6 +670,7 @@ export class DataProviderService {
lookupItem.dataProviderInfo.isPremium = false; lookupItem.dataProviderInfo.isPremium = false;
} }
lookupItem.dataProviderInfo.dataSource = undefined;
lookupItem.dataProviderInfo.name = undefined; lookupItem.dataProviderInfo.name = undefined;
lookupItem.dataProviderInfo.url = undefined; lookupItem.dataProviderInfo.url = undefined;
} else { } else {

1
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -68,6 +68,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.EOD_HISTORICAL_DATA,
isPremium: true, isPremium: true,
name: 'EOD Historical Data', name: 'EOD Historical Data',
url: 'https://eodhd.com' url: 'https://eodhd.com'

1
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -223,6 +223,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.FINANCIAL_MODELING_PREP,
isPremium: true, isPremium: true,
name: 'Financial Modeling Prep', name: 'Financial Modeling Prep',
url: 'https://financialmodelingprep.com/developer/docs' url: 'https://financialmodelingprep.com/developer/docs'

3
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -92,9 +92,10 @@ export class GhostfolioService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.GHOSTFOLIO,
isPremium: true, isPremium: true,
name: 'Ghostfolio', name: 'Ghostfolio',
url: 'https://ghostfo.io' url: 'https://ghostfol.io'
}; };
} }

1
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -47,6 +47,7 @@ export class GoogleSheetsService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.GOOGLE_SHEETS,
isPremium: false, isPremium: false,
name: 'Google Sheets', name: 'Google Sheets',
url: 'https://docs.google.com/spreadsheets' url: 'https://docs.google.com/spreadsheets'

1
apps/api/src/services/data-provider/manual/manual.service.ts

@ -64,6 +64,7 @@ export class ManualService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.MANUAL,
isPremium: false isPremium: false
}; };
} }

1
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -43,6 +43,7 @@ export class RapidApiService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.RAPID_API,
isPremium: false, isPremium: false,
name: 'Rapid API', name: 'Rapid API',
url: 'https://rapidapi.com' url: 'https://rapidapi.com'

1
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -51,6 +51,7 @@ export class YahooFinanceService implements DataProviderInterface {
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
return { return {
dataSource: DataSource.YAHOO,
isPremium: false, isPremium: false,
name: 'Yahoo Finance', name: 'Yahoo Finance',
url: 'https://finance.yahoo.com' url: 'https://finance.yahoo.com'

152
apps/client/src/app/components/admin-settings/admin-settings.component.html

@ -4,74 +4,100 @@
<h2 class="text-center" i18n>Data Providers</h2> <h2 class="text-center" i18n>Data Providers</h2>
<mat-card appearance="outlined"> <mat-card appearance="outlined">
<mat-card-content> <mat-card-content>
<div class="align-items-center d-flex my-3"> @for (dataProvider of dataProviders; track dataProvider.name) {
<div class="w-50"> <div class="align-items-center d-flex my-3">
<a @if (dataProvider.name === 'Ghostfolio') {
class="align-items-center d-inline-flex" <div class="w-50">
target="_blank" <div class="d-flex">
[href]="pricingUrl" <gf-asset-profile-icon
> class="mr-1"
@if (isGhostfolioApiKeyValid === false) { [url]="dataProvider.url"
<span class="badge badge-warning mr-1" i18n />
>Early Access</span <div>
> <a
} class="align-items-center d-inline-flex"
Ghostfolio Premium target="_blank"
<gf-premium-indicator [href]="pricingUrl"
class="d-inline-block ml-1" >
[enableLink]="false" Ghostfolio Premium
/> <gf-premium-indicator
</a> class="d-inline-block ml-1"
@if (isGhostfolioApiKeyValid === true) { [enableLink]="false"
<div class="line-height-1"> />
<small class="text-muted"> @if (isGhostfolioApiKeyValid === false) {
<ng-container i18n>Valid until</ng-container> <span class="badge badge-warning ml-2" i18n
{{ >Early Access</span
ghostfolioApiStatus?.subscription?.expiresAt >
| date: defaultDateFormat }
}}</small </a>
> @if (isGhostfolioApiKeyValid === true) {
</div> <div class="line-height-1">
} <small class="text-muted">
</div> <ng-container i18n>Valid until</ng-container>
<div class="w-50"> {{
@if (isGhostfolioApiKeyValid === true) { ghostfolioApiStatus?.subscription?.expiresAt
<div class="align-items-center d-flex flex-wrap"> | date: defaultDateFormat
<div class="flex-grow-1 mr-3"> }}</small
{{ ghostfolioApiStatus.dailyRequests }} >
<ng-container i18n>of</ng-container> </div>
{{ ghostfolioApiStatus.dailyRequestsMax }} }
<ng-container i18n>daily requests</ng-container> </div>
</div> </div>
<button </div>
class="mx-1 no-min-width px-2" <div class="w-50">
mat-button @if (isGhostfolioApiKeyValid === true) {
[matMenuTriggerFor]="ghostfolioApiMenu" <div class="align-items-center d-flex flex-wrap">
(click)="$event.stopPropagation()" <div class="flex-grow-1 mr-3">
> {{ ghostfolioApiStatus.dailyRequests }}
<ion-icon name="ellipsis-horizontal" /> <ng-container i18n>of</ng-container>
</button> {{ ghostfolioApiStatus.dailyRequestsMax }}
<mat-menu #ghostfolioApiMenu="matMenu" xPosition="before"> <ng-container i18n>daily requests</ng-container>
<button mat-menu-item (click)="onRemoveGhostfolioApiKey()"> </div>
<span class="align-items-center d-flex"> <button
<ion-icon class="mr-2" name="trash-outline" /> class="mx-1 no-min-width px-2"
<span i18n>Remove API key</span> mat-button
</span> [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> </button>
</mat-menu> }
</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>
} @else if (isGhostfolioApiKeyValid === false) { <div class="w-50"></div>
<button
color="accent"
mat-flat-button
(click)="onSetGhostfolioApiKey()"
>
<ion-icon class="mr-1" name="key-outline" />
<span i18n>Set API key</span>
</button>
} }
</div> </div>
</div> }
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>

49
apps/client/src/app/components/admin-settings/admin-settings.component.ts

@ -10,6 +10,7 @@ import {
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { import {
DataProviderGhostfolioStatusResponse, DataProviderGhostfolioStatusResponse,
DataProviderInfo,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -35,6 +36,7 @@ import { GhostfolioPremiumApiDialogParams } from './ghostfolio-premium-api-dialo
standalone: false standalone: false
}) })
export class AdminSettingsComponent implements OnDestroy, OnInit { export class AdminSettingsComponent implements OnDestroy, OnInit {
public dataProviders: DataProviderInfo[];
public defaultDateFormat: string; public defaultDateFormat: string;
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse; public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
public isGhostfolioApiKeyValid: boolean; public isGhostfolioApiKeyValid: boolean;
@ -124,23 +126,36 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
private initialize() { private initialize() {
this.adminService this.adminService
.fetchGhostfolioDataProviderStatus() .fetchAdminData()
.pipe( .pipe(takeUntil(this.unsubscribeSubject))
catchError(() => { .subscribe(({ dataProviders, settings }) => {
this.isGhostfolioApiKeyValid = false; this.dataProviders = dataProviders.filter(({ dataSource }) => {
return dataSource !== 'MANUAL';
this.changeDetectorRef.markForCheck(); });
return of(null); this.adminService
}), .fetchGhostfolioDataProviderStatus(
filter((status) => { settings[PROPERTY_API_KEY_GHOSTFOLIO] as string
return status !== null; )
}), .pipe(
takeUntil(this.unsubscribeSubject) catchError(() => {
) this.isGhostfolioApiKeyValid = false;
.subscribe((status) => {
this.ghostfolioApiStatus = status; this.changeDetectorRef.markForCheck();
this.isGhostfolioApiKeyValid = true;
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(); this.changeDetectorRef.markForCheck();
}); });

2
apps/client/src/app/components/admin-settings/admin-settings.module.ts

@ -1,5 +1,6 @@
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module'; import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
import { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.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 { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@ -17,6 +18,7 @@ import { AdminSettingsComponent } from './admin-settings.component';
CommonModule, CommonModule,
GfAdminPlatformModule, GfAdminPlatformModule,
GfAdminTagModule, GfAdminTagModule,
GfAssetProfileIconComponent,
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,

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

@ -4,8 +4,7 @@ import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { import {
HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_SKIP_INTERCEPTOR,
HEADER_KEY_TOKEN, HEADER_KEY_TOKEN
PROPERTY_API_KEY_GHOSTFOLIO
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { import {
@ -24,7 +23,6 @@ import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort'; import { SortDirection } from '@angular/material/sort';
import { DataSource, MarketData, Platform } from '@prisma/client'; import { DataSource, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull'; import { JobStatus } from 'bull';
import { switchMap } from 'rxjs';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { DataService } from './data.service'; import { DataService } from './data.service';
@ -115,19 +113,15 @@ export class AdminService {
}); });
} }
public fetchGhostfolioDataProviderStatus() { public fetchGhostfolioDataProviderStatus(aApiKey: string) {
return this.fetchAdminData().pipe( const headers = new HttpHeaders({
switchMap(({ settings }) => { [HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
const headers = new HttpHeaders({ [HEADER_KEY_TOKEN]: `Api-Key ${aApiKey}`
[HEADER_KEY_SKIP_INTERCEPTOR]: 'true', });
[HEADER_KEY_TOKEN]: `Api-Key ${settings[PROPERTY_API_KEY_GHOSTFOLIO]}`
}); return this.http.get<DataProviderGhostfolioStatusResponse>(
`${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`,
return this.http.get<DataProviderGhostfolioStatusResponse>( { headers }
`${environment.production ? 'https://ghostfol.io' : ''}/api/v2/data-providers/ghostfolio/status`,
{ headers }
);
})
); );
} }

3
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -1,4 +1,7 @@
import { DataProviderInfo } from './data-provider-info.interface';
export interface AdminData { export interface AdminData {
dataProviders: DataProviderInfo[];
settings: { [key: string]: boolean | object | string | string[] }; settings: { [key: string]: boolean | object | string | string[] };
transactionCount: number; transactionCount: number;
userCount: number; userCount: number;

3
libs/common/src/lib/interfaces/data-provider-info.interface.ts

@ -1,4 +1,7 @@
import { DataSource } from '@prisma/client';
export interface DataProviderInfo { export interface DataProviderInfo {
dataSource?: DataSource;
isPremium: boolean; isPremium: boolean;
name?: string; name?: string;
url?: string; url?: string;

Loading…
Cancel
Save