Feature/separate FIRE and X-ray pages (#4037)

* Separate FIRE / X-ray page

* Update changelog
pull/4029/head
Mohan 2 weeks ago committed by GitHub
parent 15856264f8
commit 92b025bff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the assistant by a holding selector
- Separated the _FIRE_ / _X-ray_ page
- Improved the language localization for Italian (`it`)
- Upgraded `ngx-skeleton-loader` from version `7.0.0` to `9.0.0`

@ -10,7 +10,7 @@ const routes: Routes = [
canActivate: [AuthGuard],
component: FirePageComponent,
path: '',
title: $localize`FIRE`
title: 'FIRE'
}
];

@ -1,12 +1,7 @@
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
PortfolioReport,
PortfolioReportRule,
User
} from '@ghostfolio/common/interfaces';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -21,18 +16,11 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './fire-page.html'
})
export class FirePageComponent implements OnDestroy, OnInit {
public accountClusterRiskRules: PortfolioReportRule[];
public currencyClusterRiskRules: PortfolioReportRule[];
public deviceType: string;
public economicMarketClusterRiskRules: PortfolioReportRule[];
public emergencyFundRules: PortfolioReportRule[];
public feeRules: PortfolioReportRule[];
public fireWealth: Big;
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public inactiveRules: PortfolioReportRule[];
public isLoading = false;
public isLoadingPortfolioReport = false;
public user: User;
public withdrawalRatePerMonth: Big;
public withdrawalRatePerYear: Big;
@ -95,8 +83,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
}
});
this.initializePortfolioReport();
}
public onAnnualInterestRateChange(annualInterestRate: number) {
@ -133,21 +119,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
});
});
}
public onRulesUpdated(event: UpdateUserSettingDto) {
this.dataService
.putUserSetting(event)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe();
this.initializePortfolioReport();
});
}
public onSavingsRateChange(savingsRate: number) {
this.dataService
.putUserSetting({ savingsRate })
@ -187,66 +158,4 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initializePortfolioReport() {
this.isLoadingPortfolioReport = true;
this.dataService
.fetchPortfolioReport()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioReport) => {
this.inactiveRules = this.mergeInactiveRules(portfolioReport);
this.accountClusterRiskRules =
portfolioReport.rules['accountClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
this.currencyClusterRiskRules =
portfolioReport.rules['currencyClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
this.economicMarketClusterRiskRules =
portfolioReport.rules['economicMarketClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
this.emergencyFundRules =
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.feeRules =
portfolioReport.rules['fees']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.isLoadingPortfolioReport = false;
this.changeDetectorRef.markForCheck();
});
}
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
let inactiveRules: PortfolioReportRule[] = [];
for (const category in report.rules) {
const rulesArray = report.rules[category];
inactiveRules = inactiveRules.concat(
rulesArray.filter(({ isActive }) => {
return !isActive;
})
);
}
return inactiveRules;
}
}

@ -101,133 +101,3 @@
}
</div>
</div>
<div class="container mt-5">
<div class="row">
<div class="col">
<h2 class="h3 mb-3 text-center">X-ray</h2>
<p class="mb-4">
<span i18n
>Ghostfolio X-ray uses static analysis to identify potential issues
and risks in your portfolio.</span
>
<span class="d-none"
>It will be highly configurable in the future: activate / deactivate
rules and customize the thresholds to match your personal investment
style.</span
>
</p>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Emergency Fund</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="emergencyFundRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Currency Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="currencyClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Account Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="accountClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Economic Market Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="economicMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Fees</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="feeRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
@if (inactiveRules?.length > 0) {
<div>
<h4 class="m-0" i18n>Inactive</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="inactiveRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
}
</div>
</div>
</div>

@ -1,4 +1,3 @@
import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value';
@ -17,7 +16,6 @@ import { FirePageComponent } from './fire-page.component';
FirePageRoutingModule,
GfFireCalculatorComponent,
GfPremiumIndicatorComponent,
GfRulesModule,
GfValueComponent,
NgxSkeletonLoaderModule
],

@ -34,6 +34,11 @@ const routes: Routes = [
path: 'fire',
loadChildren: () =>
import('./fire/fire-page.module').then((m) => m.FirePageModule)
},
{
path: 'x-ray',
loadChildren: () =>
import('./x-ray/x-ray-page.module').then((m) => m.XRayPageModule)
}
],
component: PortfolioPageComponent,

@ -46,8 +46,13 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
},
{
iconName: 'calculator-outline',
label: 'FIRE / X-ray',
label: 'FIRE ',
path: ['/portfolio', 'fire']
},
{
iconName: 'scan-outline',
label: 'X-ray',
path: ['/portfolio', 'x-ray']
}
];
this.user = state.user;

@ -0,0 +1,21 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { XRayPageComponent } from './x-ray-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: XRayPageComponent,
path: '',
title: 'X-ray'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class XRayPageRoutingModule {}

@ -0,0 +1,123 @@
<div class="container">
<div class="row">
<div class="col">
<h2 class="d-none d-sm-block h3 mb-3 text-center">X-ray</h2>
<p class="mb-4" i18n>
Ghostfolio X-ray uses static analysis to uncover potential issues and
risks in your portfolio. Adjust the rules below and set custom
thresholds to align with your personal investment strategy.
</p>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Emergency Fund</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="emergencyFundRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Currency Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="currencyClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Account Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="accountClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Economic Market Cluster Risks</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="economicMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div class="mb-4">
<h4 class="align-items-center d-flex m-0">
<span i18n>Fees</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="feeRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
@if (inactiveRules?.length > 0) {
<div>
<h4 class="m-0" i18n>Inactive</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoadingPortfolioReport"
[rules]="inactiveRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
}
</div>
</div>
</div>

@ -0,0 +1,150 @@
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
PortfolioReportRule,
PortfolioReport
} from '@ghostfolio/common/interfaces';
import { User } from '@ghostfolio/common/interfaces/user.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ChangeDetectorRef, Component } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
@Component({
selector: 'gf-x-ray-page',
styleUrl: './x-ray-page.component.scss',
templateUrl: './x-ray-page.component.html'
})
export class XRayPageComponent {
public accountClusterRiskRules: PortfolioReportRule[];
public currencyClusterRiskRules: PortfolioReportRule[];
public economicMarketClusterRiskRules: PortfolioReportRule[];
public emergencyFundRules: PortfolioReportRule[];
public feeRules: PortfolioReportRule[];
public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public inactiveRules: PortfolioReportRule[];
public isLoadingPortfolioReport = false;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService
) {}
public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === 'Basic'
? false
: hasPermission(
this.user.permissions,
permissions.updateUserSettings
);
this.changeDetectorRef.markForCheck();
}
});
this.initializePortfolioReport();
}
public onRulesUpdated(event: UpdateUserSettingDto) {
this.dataService
.putUserSetting(event)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe();
this.initializePortfolioReport();
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initializePortfolioReport() {
this.isLoadingPortfolioReport = true;
this.dataService
.fetchPortfolioReport()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioReport) => {
this.inactiveRules = this.mergeInactiveRules(portfolioReport);
this.accountClusterRiskRules =
portfolioReport.rules['accountClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
this.currencyClusterRiskRules =
portfolioReport.rules['currencyClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
this.economicMarketClusterRiskRules =
portfolioReport.rules['economicMarketClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
this.emergencyFundRules =
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.feeRules =
portfolioReport.rules['fees']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.isLoadingPortfolioReport = false;
this.changeDetectorRef.markForCheck();
});
}
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
let inactiveRules: PortfolioReportRule[] = [];
for (const category in report.rules) {
const rulesArray = report.rules[category];
inactiveRules = inactiveRules.concat(
rulesArray.filter(({ isActive }) => {
return !isActive;
})
);
}
return inactiveRules;
}
}

@ -0,0 +1,22 @@
import { GfRulesModule } from '@ghostfolio/client/components/rules/rules.module';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { XRayPageRoutingModule } from './x-ray-page-routing.module';
import { XRayPageComponent } from './x-ray-page.component';
@NgModule({
declarations: [XRayPageComponent],
imports: [
CommonModule,
GfPremiumIndicatorComponent,
GfRulesModule,
NgxSkeletonLoaderModule,
XRayPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class XRayPageModule {}
Loading…
Cancel
Save