diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e64f546..cc3cefd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`) diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.html b/apps/client/src/app/components/admin-settings/admin-settings.component.html index 977c8a37..997d34aa 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.html +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.html @@ -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"> diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.ts b/apps/client/src/app/components/admin-settings/admin-settings.component.ts index 68c19696..f18e49d1 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.ts +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.ts @@ -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(); }); } 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 79b269a6..c5148f68 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 @@ -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]