-
+
Asset Profiles
-
+
-
-
-
- @if (isGhostfolioDataProvider(element)) {
- @if (isGhostfolioApiKeyValid === true) {
-
-
- {{ ghostfolioApiStatus.dailyRequests }}
- of
- {{ ghostfolioApiStatus.dailyRequestsMax }}
- daily requests
-
- }
+
+
+
+ @if (
+ isGhostfolioDataProvider(element) &&
+ isGhostfolioApiKeyValid === true
+ ) {
+
+
+ {{ ghostfolioApiStatus.dailyRequests }}
+ of
+ {{ ghostfolioApiStatus.dailyRequestsMax }}
+ daily requests
+
}
-
+
-
+
@if (isGhostfolioDataProvider(element)) {
@if (isGhostfolioApiKeyValid === true) {
();
public defaultDateFormat: string;
- public displayedColumns = ['name', 'assetProfileCount', 'status', 'actions'];
+ public displayedColumns = [
+ 'name',
+ 'status',
+ 'assetProfileCount',
+ 'usage',
+ 'actions'
+ ];
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
public isGhostfolioApiKeyValid: boolean;
public isLoading = false;
diff --git a/apps/client/src/app/components/admin-settings/admin-settings.module.ts b/apps/client/src/app/components/admin-settings/admin-settings.module.ts
index 52aec67be..e6bef6f7b 100644
--- a/apps/client/src/app/components/admin-settings/admin-settings.module.ts
+++ b/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 { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.module';
+import { GfDataProviderStatusComponent } from '@ghostfolio/client/components/data-provider-status/data-provider-status.component';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
@@ -22,6 +23,7 @@ import { AdminSettingsComponent } from './admin-settings.component';
CommonModule,
GfAdminPlatformModule,
GfAdminTagModule,
+ GfDataProviderStatusComponent,
GfEntityLogoComponent,
GfPremiumIndicatorComponent,
GfValueComponent,
diff --git a/apps/client/src/app/components/data-provider-status/data-provider-status.component.html b/apps/client/src/app/components/data-provider-status/data-provider-status.component.html
new file mode 100644
index 000000000..d2f26b09b
--- /dev/null
+++ b/apps/client/src/app/components/data-provider-status/data-provider-status.component.html
@@ -0,0 +1,15 @@
+@if (status$ | async; as status) {
+ @if (status.isHealthy) {
+ Online
+ } @else {
+ Offline
+ }
+} @else {
+
+}
diff --git a/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts b/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts
new file mode 100644
index 000000000..ddd505591
--- /dev/null
+++ b/apps/client/src/app/components/data-provider-status/data-provider-status.component.ts
@@ -0,0 +1,51 @@
+import { DataService } from '@ghostfolio/client/services/data.service';
+
+import { CommonModule } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Input,
+ OnDestroy,
+ OnInit
+} from '@angular/core';
+import type { DataSource } from '@prisma/client';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { catchError, map, type Observable, of, Subject, takeUntil } from 'rxjs';
+
+import { DataProviderStatus } from './interfaces/interfaces';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, NgxSkeletonLoaderModule],
+ selector: 'gf-data-provider-status',
+ standalone: true,
+ templateUrl: './data-provider-status.component.html'
+})
+export class GfDataProviderStatusComponent implements OnDestroy, OnInit {
+ @Input() dataSource: DataSource;
+
+ public status$: Observable;
+
+ private unsubscribeSubject = new Subject();
+
+ public constructor(private dataService: DataService) {}
+
+ public ngOnInit() {
+ this.status$ = this.dataService
+ .fetchDataProviderHealth(this.dataSource)
+ .pipe(
+ catchError(() => {
+ return of({ isHealthy: false });
+ }),
+ map(() => {
+ return { isHealthy: true };
+ }),
+ takeUntil(this.unsubscribeSubject)
+ );
+ }
+
+ public ngOnDestroy() {
+ this.unsubscribeSubject.next();
+ this.unsubscribeSubject.complete();
+ }
+}
diff --git a/apps/client/src/app/components/data-provider-status/interfaces/interfaces.ts b/apps/client/src/app/components/data-provider-status/interfaces/interfaces.ts
new file mode 100644
index 000000000..2c8071899
--- /dev/null
+++ b/apps/client/src/app/components/data-provider-status/interfaces/interfaces.ts
@@ -0,0 +1,3 @@
+export interface DataProviderStatus {
+ isHealthy: boolean;
+}
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts
index e3bd9f27b..ab36109cd 100644
--- a/apps/client/src/app/services/data.service.ts
+++ b/apps/client/src/app/services/data.service.ts
@@ -30,6 +30,7 @@ import {
AssetProfileIdentifier,
BenchmarkMarketDataDetails,
BenchmarkResponse,
+ DataProviderHealthResponse,
Export,
Filter,
ImportResponse,
@@ -380,6 +381,12 @@ export class DataService {
return this.http.get('/api/v1/benchmarks');
}
+ public fetchDataProviderHealth(dataSource: DataSource) {
+ return this.http.get(
+ `/api/v1/health/data-provider/${dataSource}`
+ );
+ }
+
public fetchExport({
activityIds,
filters