From 132f8670b971dcc381555a10e1860ba06c221638 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:41:34 +0200 Subject: [PATCH] Task/improve usability in admin control panel by eliminating page reloads (#7518) * Improve usability by eliminating page reloads * Update changelog --- CHANGELOG.md | 7 +++ .../admin-market-data.component.ts | 62 ++++++++++++++----- .../admin-market-data.service.ts | 17 +++-- .../asset-profile-dialog.component.ts | 9 ++- .../admin-overview.component.ts | 41 +++++++++--- .../admin-overview/admin-overview.html | 15 +++-- .../user-account-membership.component.ts | 23 ++----- 7 files changed, 120 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 123ef1f94..a916f8643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the database model and endpoints to manage the stock splits of an asset profile (experimental) +### Changed + +- Improved the usability of the admin control panel by eliminating the page reload on changing a setting +- Improved the usability of the admin control panel by eliminating the page reload on deleting an asset profile +- Improved the usability of the admin control panel by eliminating the page reload on flushing the cache +- Improved the usability of the admin control panel by eliminating the page reload on gathering historical market data + ### Fixed - Fixed the loading state in the user detail dialog of the admin control panelโ€™s users section diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index b29ccc7d7..8592070ee 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -43,6 +43,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSort, MatSortModule, @@ -64,6 +65,7 @@ import { ellipsisVertical, trashOutline } from 'ionicons/icons'; +import ms from 'ms'; import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { Subject } from 'rxjs'; @@ -88,6 +90,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in MatCheckboxModule, MatMenuModule, MatPaginatorModule, + MatSnackBarModule, MatSortModule, MatTableModule, NgxSkeletonLoaderModule, @@ -177,6 +180,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { private readonly dialog = inject(MatDialog); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); + private readonly snackBar = inject(MatSnackBar); private readonly userService = inject(UserService); public constructor() { @@ -239,7 +243,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { .subscribe((filters) => { this.activeFilters = filters; - this.loadData(); + this.reloadData({ pageIndex: 0 }); }); addIcons({ @@ -285,15 +289,25 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { dataSource, symbol }: AssetProfileIdentifier) { - this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); + this.adminMarketDataService + .deleteAssetProfile({ dataSource, symbol }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.reloadData(); + }); } protected onDeleteAssetProfiles() { - this.adminMarketDataService.deleteAssetProfiles( - this.selection.selected.map(({ dataSource, symbol }) => { - return { dataSource, symbol }; - }) - ); + this.adminMarketDataService + .deleteAssetProfiles( + this.selection.selected.map(({ dataSource, symbol }) => { + return { dataSource, symbol }; + }) + ) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.reloadData(); + }); } protected onGatherMax() { @@ -301,9 +315,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { .gatherMax() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + this.notifyDataGatheringHasBeenStarted(); }); } @@ -311,7 +323,9 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { this.adminService .gatherProfileData() .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); + .subscribe(() => { + this.notifyDataGatheringHasBeenStarted(); + }); } protected onGatherRecentMarketData() { @@ -319,9 +333,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { .gatherRecentMarketData() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + this.notifyDataGatheringHasBeenStarted(); }); } @@ -396,6 +408,16 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }); } + private notifyDataGatheringHasBeenStarted() { + this.snackBar.open( + 'โœ… ' + $localize`Data gathering has been started.`, + undefined, + { + duration: ms('3 seconds') + } + ); + } + private openAssetProfileDialog({ dataSource, symbol @@ -431,6 +453,8 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { if (newAssetProfileIdentifier) { this.onOpenAssetProfileDialog(newAssetProfileIdentifier); } else { + this.reloadData(); + this.router.navigate(['.'], { relativeTo: this.route }); } }); @@ -483,4 +507,14 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { }); }); } + + private reloadData({ + pageIndex = this.paginator().pageIndex + }: { pageIndex?: number } = {}) { + this.loadData({ + pageIndex, + sortColumn: this.sort().active, + sortDirection: this.sort().direction + }); + } } diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts index 28c8c2d9f..c4d45b7f0 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts @@ -4,7 +4,7 @@ import { NotificationService } from '@ghostfolio/ui/notifications'; import { AdminService } from '@ghostfolio/ui/services'; import { Injectable } from '@angular/core'; -import { EMPTY, catchError, finalize, forkJoin } from 'rxjs'; +import { EMPTY, Subject, catchError, finalize, forkJoin } from 'rxjs'; @Injectable() export class AdminMarketDataService { @@ -14,25 +14,29 @@ export class AdminMarketDataService { ) {} public deleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) { + const assetProfileDeleted = new Subject(); + this.notificationService.confirm({ confirmFn: () => { this.adminService .deleteProfileData({ dataSource, symbol }) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + assetProfileDeleted.next(); + assetProfileDeleted.complete(); }); }, confirmType: ConfirmationDialogType.Warn, title: $localize`Do you really want to delete this asset profile?` }); + + return assetProfileDeleted.asObservable(); } public deleteAssetProfiles( aAssetProfileIdentifiers: AssetProfileIdentifier[] ) { const assetProfileCount = aAssetProfileIdentifiers.length; + const assetProfilesDeleted = new Subject(); this.notificationService.confirm({ confirmFn: () => { @@ -55,7 +59,8 @@ export class AdminMarketDataService { return EMPTY; }), finalize(() => { - window.location.reload(); + assetProfilesDeleted.next(); + assetProfilesDeleted.complete(); }) ) .subscribe(); @@ -66,5 +71,7 @@ export class AdminMarketDataService { ? $localize`Do you really want to delete this asset profile?` : $localize`Do you really want to delete these ${assetProfileCount}:count: asset profiles?` }); + + return assetProfilesDeleted.asObservable(); } } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index f0e8eaf17..aa61845f4 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -531,9 +531,12 @@ export class GfAssetProfileDialogComponent implements OnInit { dataSource, symbol }: AssetProfileIdentifier) { - this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); - - this.dialogRef.close(); + this.adminMarketDataService + .deleteAssetProfile({ dataSource, symbol }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.dialogRef.close(); + }); } protected onGatherProfileDataBySymbol({ diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 0bed8111f..733200c91 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -64,6 +64,7 @@ import { } from 'ionicons/icons'; import ms, { StringValue } from 'ms'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { catchError, of, switchMap } from 'rxjs'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -105,6 +106,8 @@ export class GfAdminOverviewComponent implements OnInit { protected readonly info: InfoItem; protected isDataGatheringEnabled: boolean; protected isLoading = false; + protected isReadOnlyMode: boolean; + protected isUserSignupEnabled: boolean; protected readonly permissions = permissions; protected systemMessage: SystemMessage; protected userCount: number; @@ -179,6 +182,8 @@ export class GfAdminOverviewComponent implements OnInit { } public ngOnInit() { + this.isLoading = true; + this.fetchAdminData(); } @@ -270,9 +275,15 @@ export class GfAdminOverviewComponent implements OnInit { .flush() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + this.dataService.updateInfo(); + + this.snackBar.open( + 'โœ… ' + $localize`Cache has been flushed.`, + undefined, + { + duration: ms('3 seconds') + } + ); }); }, confirmType: ConfirmationDialogType.Warn, @@ -330,8 +341,6 @@ export class GfAdminOverviewComponent implements OnInit { } private fetchAdminData() { - this.isLoading = true; - this.adminService .fetchAdminData() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -344,6 +353,11 @@ export class GfAdminOverviewComponent implements OnInit { this.isDataGatheringEnabled = settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true; + this.isReadOnlyMode = settings[PROPERTY_IS_READ_ONLY_MODE] === true; + + this.isUserSignupEnabled = + settings[PROPERTY_IS_USER_SIGNUP_ENABLED] === false ? false : true; + this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage; this.userCount = userCount; this.version = version; @@ -372,11 +386,20 @@ export class GfAdminOverviewComponent implements OnInit { .putAdminSetting(key, { value: value || value === false ? JSON.stringify(value) : undefined }) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe( + switchMap(() => { + return this.userService.get(true); + }), + catchError(() => { + // Refresh anyway to reflect the actual state of the settings + return of(undefined); + }), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + this.dataService.updateInfo(); + + this.fetchAdminData(); }); } diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index 7e05600a6..b919f085f 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -52,9 +52,8 @@ @@ -66,7 +65,8 @@ @@ -79,6 +79,7 @@ color="primary" hideIcon="true" [checked]="isDataGatheringEnabled" + [disabled]="isLoading" (change)="onEnableDataGatheringChange($event)" /> @@ -89,7 +90,9 @@
@if (systemMessage) {
-
{{ systemMessage | json }}
+
+ {{ systemMessage | json }} +
} - @if (!info?.systemMessage) { + @if (!systemMessage) {