Browse Source

Task/improve usability in admin control panel by eliminating page reloads (#7518)

* Improve usability by eliminating page reloads

* Update changelog
pull/7501/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
132f8670b9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      CHANGELOG.md
  2. 56
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  3. 17
      apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
  4. 7
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  5. 41
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  6. 15
      apps/client/src/app/components/admin-overview/admin-overview.html
  7. 23
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

7
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

56
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.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
});
}
}

17
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<void>();
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<void>();
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();
}
}

7
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.adminMarketDataService
.deleteAssetProfile({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.dialogRef.close();
});
}
protected onGatherProfileDataBySymbol({

41
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();
});
}

15
apps/client/src/app/components/admin-overview/admin-overview.html

@ -52,9 +52,8 @@
<mat-slide-toggle
color="primary"
hideIcon="true"
[checked]="
info.globalPermissions.includes(permissions.createUserAccount)
"
[checked]="isUserSignupEnabled"
[disabled]="isLoading"
(change)="onEnableUserSignupModeChange($event)"
/>
</div>
@ -66,7 +65,8 @@
<mat-slide-toggle
color="primary"
hideIcon="true"
[checked]="info?.isReadOnlyMode"
[checked]="isReadOnlyMode"
[disabled]="isLoading"
(change)="onReadOnlyModeChange($event)"
/>
</div>
@ -79,6 +79,7 @@
color="primary"
hideIcon="true"
[checked]="isDataGatheringEnabled"
[disabled]="isLoading"
(change)="onEnableDataGatheringChange($event)"
/>
</div>
@ -89,7 +90,9 @@
<div class="w-50">
@if (systemMessage) {
<div class="align-items-center d-flex">
<div class="text-truncate">{{ systemMessage | json }}</div>
<div class="text-truncate">
{{ systemMessage | json }}
</div>
<button
class="h-100 mx-1 no-min-width px-2"
mat-button
@ -99,7 +102,7 @@
</button>
</div>
}
@if (!info?.systemMessage) {
@if (!systemMessage) {
<button
class="mt-2"
color="accent"

23
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -24,7 +24,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
import ms, { StringValue } from 'ms';
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { catchError, switchMap } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@ -173,6 +173,9 @@ export class GfUserAccountMembershipComponent {
this.dataService
.redeemCoupon(couponCode)
.pipe(
switchMap(() => {
return this.userService.get(true);
}),
catchError(() => {
this.snackBar.open(
'😞 ' + $localize`Could not redeem coupon code`,
@ -187,27 +190,13 @@ export class GfUserAccountMembershipComponent {
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
const snackBarRef = this.snackBar.open(
this.snackBar.open(
'✅ ' + $localize`Coupon code has been redeemed`,
$localize`Reload`,
undefined,
{
duration: ms('3 seconds')
}
);
snackBarRef
.afterDismissed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
window.location.reload();
});
snackBarRef
.onAction()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
window.location.reload();
});
});
}
},

Loading…
Cancel
Save