Feature/migrate data providers overview to Angular Material table (#4704)

* Migrate data providers overview to Angular Material table

* Update changelog
This commit is contained in:
andiz2 2025-05-10 21:23:06 +03:00 committed by GitHub
parent 4adc9dc9b1
commit 0b7fc7a3b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 115 additions and 93 deletions

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Harmonized the data providers management style of the admin control panel
- Renamed `Order` to `activities` in the `User` database schema - Renamed `Order` to `activities` in the `User` database schema
- Improved the language localization for Catalan (`ca`) - Improved the language localization for Catalan (`ca`)
- Improved the language localization for Chinese (`zh`) - Improved the language localization for Chinese (`zh`)

View File

@ -1,105 +1,109 @@
<div class="container"> <div class="container">
<div class="d-md-block d-none mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h2 class="text-center" i18n>Data Providers</h2> <h2 class="text-center" i18n>Data Providers</h2>
<mat-card appearance="outlined"> <table class="gf-table w-100" mat-table [dataSource]="dataSource">
<mat-card-content> <ng-container matColumnDef="name">
@for (dataProvider of dataProviders; track dataProvider.name) { <th *matHeaderCellDef class="px-1 py-2" mat-header-cell>
<div class="align-items-center d-flex my-3"> <ng-container i18n>Name</ng-container>
@if (dataProvider.name === 'Ghostfolio') { </th>
<div class="w-50"> <td *matCellDef="let element" class="px-1 py-2" mat-cell>
<div class="d-flex"> <div class="d-flex align-items-center">
<gf-asset-profile-icon <gf-asset-profile-icon class="mr-1" [url]="element.url" />
class="mr-1" <div>
[url]="dataProvider.url" @if (isGhostfolioDataProvider(element)) {
<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"
/> />
<div> @if (isGhostfolioApiKeyValid === false) {
<a <span class="badge badge-warning ml-2" i18n
class="align-items-center d-inline-flex" >Early Access</span
target="_blank"
[href]="pricingUrl"
> >
Ghostfolio Premium }
<gf-premium-indicator </a>
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>
</div>
<div class="w-50">
@if (isGhostfolioApiKeyValid === true) { @if (isGhostfolioApiKeyValid === true) {
<div class="align-items-center d-flex flex-wrap"> <div class="line-height-1">
<div class="flex-grow-1 mr-3"> <small class="text-muted">
<ng-container i18n>Valid until</ng-container>
{{
ghostfolioApiStatus?.subscription?.expiresAt
| date: defaultDateFormat
}}
</small>
</div>
<div class="line-height-1 mt-1">
<small class="text-muted">
{{ ghostfolioApiStatus.dailyRequests }} {{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>of</ng-container> <ng-container i18n>of</ng-container>
{{ ghostfolioApiStatus.dailyRequestsMax }} {{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container> <ng-container i18n>daily requests</ng-container>
</div> </small>
<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> </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 {
} @else { {{ element.name }}
<div class="w-50"> }
<div class="d-flex"> </div>
<gf-asset-profile-icon
class="mr-1"
[url]="dataProvider.url"
/>
{{ dataProvider.name }}
</div>
</div>
<div class="w-50"></div>
}
</div> </div>
} </td>
</mat-card-content> </ng-container>
</mat-card>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 py-2 text-right" mat-cell>
@if (isGhostfolioDataProvider(element)) {
@if (isGhostfolioApiKeyValid === true) {
<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>
} @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>
}
}
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
@if (isLoading) {
<ngx-skeleton-loader
animation="pulse"
class="px-4 py-3"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
}
</div> </div>
</div> </div>
<div class="mb-5 row"> <div class="mb-5 row">

View File

@ -22,6 +22,7 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { catchError, filter, of, Subject, takeUntil } from 'rxjs'; import { catchError, filter, of, Subject, takeUntil } from 'rxjs';
@ -36,10 +37,12 @@ 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 dataSource = new MatTableDataSource<DataProviderInfo>();
public defaultDateFormat: string; public defaultDateFormat: string;
public displayedColumns = ['name', 'actions'];
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse; public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
public isGhostfolioApiKeyValid: boolean; public isGhostfolioApiKeyValid: boolean;
public isLoading = false;
public pricingUrl: string; public pricingUrl: string;
private deviceType: string; private deviceType: string;
@ -83,6 +86,10 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
this.initialize(); this.initialize();
} }
public isGhostfolioDataProvider(provider: DataProviderInfo): boolean {
return provider.dataSource === 'GHOSTFOLIO';
}
public onRemoveGhostfolioApiKey() { public onRemoveGhostfolioApiKey() {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
@ -125,14 +132,20 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
} }
private initialize() { private initialize() {
this.isLoading = true;
this.dataSource = new MatTableDataSource();
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ dataProviders, settings }) => { .subscribe(({ dataProviders, settings }) => {
this.dataProviders = dataProviders.filter(({ dataSource }) => { const filteredProviders = dataProviders.filter(({ dataSource }) => {
return dataSource !== 'MANUAL'; return dataSource !== 'MANUAL';
}); });
this.dataSource = new MatTableDataSource(filteredProviders);
this.adminService this.adminService
.fetchGhostfolioDataProviderStatus( .fetchGhostfolioDataProviderStatus(
settings[PROPERTY_API_KEY_GHOSTFOLIO] as string settings[PROPERTY_API_KEY_GHOSTFOLIO] as string
@ -157,6 +170,8 @@ export class AdminSettingsComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.isLoading = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }

View File

@ -6,9 +6,10 @@ import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AdminSettingsComponent } from './admin-settings.component'; import { AdminSettingsComponent } from './admin-settings.component';
@ -21,8 +22,9 @@ import { AdminSettingsComponent } from './admin-settings.component';
GfAssetProfileIconComponent, GfAssetProfileIconComponent,
GfPremiumIndicatorComponent, GfPremiumIndicatorComponent,
MatButtonModule, MatButtonModule,
MatCardModule,
MatMenuModule, MatMenuModule,
MatTableModule,
NgxSkeletonLoaderModule,
RouterModule RouterModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]