Browse Source

Feature/add status column to data providers table (#4998)

* Add status column to data providers table

* Update changelog
pull/5008/head
Kenrick Tandrian 1 week ago
committed by GitHub
parent
commit
b6854f30e1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 69
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  3. 8
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  4. 2
      apps/client/src/app/components/admin-settings/admin-settings.module.ts
  5. 15
      apps/client/src/app/components/data-provider-status/data-provider-status.component.html
  6. 51
      apps/client/src/app/components/data-provider-status/data-provider-status.component.ts
  7. 3
      apps/client/src/app/components/data-provider-status/interfaces/interfaces.ts
  8. 7
      apps/client/src/app/services/data.service.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the data providers management of the admin control panel by the online status
### Changed ### Changed
- Migrated the `@ghostfolio/ui/value` component to control flow - Migrated the `@ghostfolio/ui/value` component to control flow

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

@ -40,10 +40,10 @@
} }
<table class="gf-table w-100" mat-table [dataSource]="dataSource"> <table class="gf-table w-100" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell> <th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Name</ng-container> <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 py-2" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<gf-entity-logo class="mr-1" [url]="element.url" /> <gf-entity-logo class="mr-1" [url]="element.url" />
<div> <div>
@ -85,11 +85,25 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="status">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Status</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
@if (
!isGhostfolioDataProvider(element) ||
isGhostfolioApiKeyValid === true
) {
<gf-data-provider-status [dataSource]="element.dataSource" />
}
</td>
</ng-container>
<ng-container matColumnDef="assetProfileCount"> <ng-container matColumnDef="assetProfileCount">
<th *matHeaderCellDef class="px-1 py-2 text-right" mat-header-cell> <th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<ng-container i18n>Asset Profiles</ng-container> <ng-container i18n>Asset Profiles</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 py-2 text-right" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -98,35 +112,36 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <ng-container matColumnDef="usage">
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell></th> <th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 py-2" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
@if (isGhostfolioDataProvider(element)) { @if (
@if (isGhostfolioApiKeyValid === true) { isGhostfolioDataProvider(element) &&
<mat-progress-bar isGhostfolioApiKeyValid === true
mode="determinate" ) {
[value]=" <mat-progress-bar
100 - mode="determinate"
(ghostfolioApiStatus.dailyRequests / [value]="
ghostfolioApiStatus.dailyRequestsMax) * 100 -
100 (ghostfolioApiStatus.dailyRequests /
" ghostfolioApiStatus.dailyRequestsMax) *
/> 100
<small class="text-muted"> "
{{ ghostfolioApiStatus.dailyRequests }} />
<ng-container i18n>of</ng-container> <small class="text-muted">
{{ ghostfolioApiStatus.dailyRequestsMax }} {{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>daily requests</ng-container> <ng-container i18n>of</ng-container>
</small> {{ ghostfolioApiStatus.dailyRequestsMax }}
} <ng-container i18n>daily requests</ng-container>
</small>
} }
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell></th> <th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 py-2 text-right" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
@if (isGhostfolioDataProvider(element)) { @if (isGhostfolioDataProvider(element)) {
@if (isGhostfolioApiKeyValid === true) { @if (isGhostfolioApiKeyValid === true) {
<button <button

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

@ -32,7 +32,13 @@ import { catchError, filter, of, Subject, takeUntil } from 'rxjs';
export class AdminSettingsComponent implements OnDestroy, OnInit { export class AdminSettingsComponent implements OnDestroy, OnInit {
public dataSource = new MatTableDataSource<DataProviderInfo>(); public dataSource = new MatTableDataSource<DataProviderInfo>();
public defaultDateFormat: string; public defaultDateFormat: string;
public displayedColumns = ['name', 'assetProfileCount', 'status', 'actions']; public displayedColumns = [
'name',
'status',
'assetProfileCount',
'usage',
'actions'
];
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse; public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
public isGhostfolioApiKeyValid: boolean; public isGhostfolioApiKeyValid: boolean;
public isLoading = false; public isLoading = false;

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 { GfDataProviderStatusComponent } from '@ghostfolio/client/components/data-provider-status/data-provider-status.component';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -22,6 +23,7 @@ import { AdminSettingsComponent } from './admin-settings.component';
CommonModule, CommonModule,
GfAdminPlatformModule, GfAdminPlatformModule,
GfAdminTagModule, GfAdminTagModule,
GfDataProviderStatusComponent,
GfEntityLogoComponent, GfEntityLogoComponent,
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
GfValueComponent, GfValueComponent,

15
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) {
<span class="text-success" i18n>Online</span>
} @else {
<span class="text-danger" i18n>Offline</span>
}
} @else {
<ngx-skeleton-loader
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
}

51
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<DataProviderStatus>;
private unsubscribeSubject = new Subject<void>();
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();
}
}

3
apps/client/src/app/components/data-provider-status/interfaces/interfaces.ts

@ -0,0 +1,3 @@
export interface DataProviderStatus {
isHealthy: boolean;
}

7
apps/client/src/app/services/data.service.ts

@ -30,6 +30,7 @@ import {
AssetProfileIdentifier, AssetProfileIdentifier,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
DataProviderHealthResponse,
Export, Export,
Filter, Filter,
ImportResponse, ImportResponse,
@ -380,6 +381,12 @@ export class DataService {
return this.http.get<BenchmarkResponse>('/api/v1/benchmarks'); return this.http.get<BenchmarkResponse>('/api/v1/benchmarks');
} }
public fetchDataProviderHealth(dataSource: DataSource) {
return this.http.get<DataProviderHealthResponse>(
`/api/v1/health/data-provider/${dataSource}`
);
}
public fetchExport({ public fetchExport({
activityIds, activityIds,
filters filters

Loading…
Cancel
Save