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
- Harmonized the data providers management style of the admin control panel
- Renamed `Order` to `activities` in the `User` database schema
- Improved the language localization for Catalan (`ca`)
- Improved the language localization for Chinese (`zh`)

View File

@ -1,105 +1,109 @@
<div class="container">
<div class="d-md-block d-none mb-5 row">
<div class="mb-5 row">
<div class="col">
<h2 class="text-center" i18n>Data Providers</h2>
<mat-card appearance="outlined">
<mat-card-content>
@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"
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="name">
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1 py-2" mat-cell>
<div class="d-flex align-items-center">
<gf-asset-profile-icon class="mr-1" [url]="element.url" />
<div>
@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>
<a
class="align-items-center d-inline-flex"
target="_blank"
[href]="pricingUrl"
@if (isGhostfolioApiKeyValid === false) {
<span class="badge badge-warning ml-2" i18n
>Early Access</span
>
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>
</div>
<div class="w-50">
}
</a>
@if (isGhostfolioApiKeyValid === true) {
<div class="align-items-center d-flex flex-wrap">
<div class="flex-grow-1 mr-3">
<div class="line-height-1">
<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 }}
<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>
</small>
</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>
}
} @else {
{{ element.name }}
}
</div>
</div>
}
</mat-card-content>
</mat-card>
</td>
</ng-container>
<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 class="mb-5 row">

View File

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

View File

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