Browse Source

Task/improve type safety in asset profile dialog (#6722)

* Improve type safety
pull/6711/merge
Kenrick Tandrian 4 days ago
committed by GitHub
parent
commit
d5ff1bea56
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 299
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  2. 4
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

299
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -87,6 +87,7 @@ import {
readerOutline, readerOutline,
serverOutline serverOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isBoolean } from 'lodash';
import ms from 'ms'; import ms from 'ms';
import { EMPTY } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@ -129,25 +130,26 @@ export class GfAssetProfileDialogComponent implements OnInit {
)};123.45`; )};123.45`;
@ViewChild('assetProfileFormElement') @ViewChild('assetProfileFormElement')
assetProfileFormElement: ElementRef<HTMLFormElement>; public readonly assetProfileFormElement: ElementRef<HTMLFormElement>;
public assetClassLabel: string; protected assetClassLabel: string;
public assetSubClassLabel: string; protected assetSubClassLabel: string;
public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass) protected readonly assetClassOptions: AssetClassSelectorOption[] =
.map((id) => { Object.keys(AssetClass)
return { id, label: translate(id) } as AssetClassSelectorOption; .map((id) => {
}) return { id, label: translate(id) } as AssetClassSelectorOption;
.sort((a, b) => { })
return a.label.localeCompare(b.label); .sort((a, b) => {
}); return a.label.localeCompare(b.label);
});
public assetSubClassOptions: AssetClassSelectorOption[] = []; protected assetSubClassOptions: AssetClassSelectorOption[] = [];
public assetProfile: AdminMarketDataDetails['assetProfile']; protected assetProfile: AdminMarketDataDetails['assetProfile'];
public assetProfileForm = this.formBuilder.group({ protected readonly assetProfileForm = this.formBuilder.group({
assetClass: new FormControl<AssetClass>(undefined), assetClass: new FormControl<AssetClass | null>(null),
assetSubClass: new FormControl<AssetSubClass>(undefined), assetSubClass: new FormControl<AssetSubClass | null>(null),
comment: '', comment: '',
countries: ['', jsonValidator()], countries: ['', jsonValidator()],
currency: '', currency: '',
@ -156,11 +158,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
}), }),
isActive: [true], isActive: [true],
name: ['', Validators.required], name: ['', Validators.required],
scraperConfiguration: this.formBuilder.group({ scraperConfiguration: this.formBuilder.group<
defaultMarketPrice: null, Omit<ScraperConfiguration, 'headers'> & {
headers: [JSON.stringify({}), jsonValidator()], headers: FormControl<string | null>;
}
>({
defaultMarketPrice: undefined,
headers: new FormControl(JSON.stringify({}), jsonValidator()),
locale: '', locale: '',
mode: '', mode: 'lazy',
selector: '', selector: '',
url: '' url: ''
}), }),
@ -169,12 +175,11 @@ export class GfAssetProfileDialogComponent implements OnInit {
url: '' url: ''
}); });
public assetProfileIdentifierForm = this.formBuilder.group( protected readonly assetProfileIdentifierForm = this.formBuilder.group(
{ {
assetProfileIdentifier: new FormControl<AssetProfileIdentifier>( assetProfileIdentifier: new FormControl<
{ symbol: null, dataSource: null }, AssetProfileIdentifier | { dataSource: null; symbol: null }
[Validators.required] >({ dataSource: null, symbol: null }, [Validators.required])
)
}, },
{ {
validators: (control) => { validators: (control) => {
@ -183,16 +188,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
); );
public benchmarks: Partial<SymbolProfile>[]; protected canEditAssetProfile = true;
public canEditAssetProfile = true;
public countries: { protected countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public currencies: string[] = []; protected currencies: string[] = [];
public dateRangeOptions = [ protected readonly dateRangeOptions = [
{ {
label: $localize`Current week` + ' (' + $localize`WTD` + ')', label: $localize`Current week` + ' (' + $localize`WTD` + ')',
value: 'wtd' value: 'wtd'
@ -218,14 +222,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
value: 'max' value: 'max'
} }
]; ];
public historicalDataItems: LineChartItem[]; protected historicalDataItems: LineChartItem[];
public isBenchmark = false; protected isBenchmark = false;
public isDataGatheringEnabled: boolean; protected isDataGatheringEnabled: boolean;
public isEditAssetProfileIdentifierMode = false; protected isEditAssetProfileIdentifierMode = false;
public isUUID = isUUID; protected readonly isUUID = isUUID;
public marketDataItems: MarketData[] = []; protected marketDataItems: MarketData[] = [];
public modeValues = [ protected readonly modeValues = [
{ {
value: 'lazy', value: 'lazy',
viewValue: $localize`Lazy` + ' (' + $localize`end of day` + ')' viewValue: $localize`Lazy` + ' (' + $localize`end of day` + ')'
@ -236,20 +240,22 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
]; ];
public sectors: { protected sectors: {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
public user: User; protected user: User;
private benchmarks: Partial<SymbolProfile>[];
public constructor( public constructor(
public adminMarketDataService: AdminMarketDataService, protected adminMarketDataService: AdminMarketDataService,
private adminService: AdminService, private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams, @Inject(MAT_DIALOG_DATA) protected data: AssetProfileDialogParams,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfAssetProfileDialogComponent>, private dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private notificationService: NotificationService, private notificationService: NotificationService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
@ -264,7 +270,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
}); });
} }
public get canSaveAssetProfileIdentifier() { protected get canSaveAssetProfileIdentifier() {
return !this.assetProfileForm.dirty && this.canEditAssetProfile; return !this.assetProfileForm.dirty && this.canEditAssetProfile;
} }
@ -277,8 +283,8 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.initialize(); this.initialize();
} }
public initialize() { protected initialize() {
this.historicalDataItems = undefined; this.historicalDataItems = [];
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
@ -298,10 +304,13 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
}); });
this.assetProfileForm this.assetProfileForm.controls.assetClass.valueChanges
.get('assetClass') .pipe(takeUntilDestroyed(this.destroyRef))
.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((assetClass) => { .subscribe((assetClass) => {
if (!assetClass) {
return;
}
const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? []; const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];
this.assetSubClassOptions = assetSubClasses this.assetSubClassOptions = assetSubClasses
@ -313,7 +322,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
}) })
.sort((a, b) => a.label.localeCompare(b.label)); .sort((a, b) => a.label.localeCompare(b.label));
this.assetProfileForm.get('assetSubClass').setValue(null); this.assetProfileForm.controls.assetSubClass.setValue(null);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -327,8 +336,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe(({ assetProfile, marketData }) => { .subscribe(({ assetProfile, marketData }) => {
this.assetProfile = assetProfile; this.assetProfile = assetProfile;
this.assetClassLabel = translate(this.assetProfile?.assetClass); this.assetClassLabel = translate(this.assetProfile?.assetClass ?? '');
this.assetSubClassLabel = translate(this.assetProfile?.assetSubClass); this.assetSubClassLabel = translate(
this.assetProfile?.assetSubClass ?? ''
);
this.canEditAssetProfile = !isCurrency( this.canEditAssetProfile = !isCurrency(
getCurrencyFromSymbol(this.data.symbol) getCurrencyFromSymbol(this.data.symbol)
@ -350,7 +361,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.marketDataItems = marketData; this.marketDataItems = marketData;
this.sectors = {}; this.sectors = {};
if (this.assetProfile?.countries?.length > 0) { if (
this.assetProfile?.countries &&
this.assetProfile.countries.length > 0
) {
for (const { code, name, weight } of this.assetProfile.countries) { for (const { code, name, weight } of this.assetProfile.countries) {
this.countries[code] = { this.countries[code] = {
name, name,
@ -359,7 +373,10 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
} }
if (this.assetProfile?.sectors?.length > 0) { if (
this.assetProfile?.sectors &&
this.assetProfile.sectors.length > 0
) {
for (const { name, weight } of this.assetProfile.sectors) { for (const { name, weight } of this.assetProfile.sectors) {
this.sectors[name] = { this.sectors[name] = {
name, name,
@ -377,12 +394,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
return { code, weight }; return { code, weight };
}) ?? [] }) ?? []
), ),
currency: this.assetProfile?.currency, currency: this.assetProfile?.currency ?? null,
historicalData: { historicalData: {
csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE
}, },
isActive: this.assetProfile?.isActive, isActive: isBoolean(this.assetProfile?.isActive)
name: this.assetProfile.name ?? this.assetProfile.symbol, ? this.assetProfile.isActive
: null,
name: this.assetProfile.name ?? this.assetProfile.symbol ?? null,
scraperConfiguration: { scraperConfiguration: {
defaultMarketPrice: defaultMarketPrice:
this.assetProfile?.scraperConfiguration?.defaultMarketPrice ?? this.assetProfile?.scraperConfiguration?.defaultMarketPrice ??
@ -410,7 +429,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
}); });
} }
public onCancelEditAssetProfileIdentifierMode() { protected onCancelEditAssetProfileIdentifierMode() {
this.isEditAssetProfileIdentifierMode = false; this.isEditAssetProfileIdentifierMode = false;
if (this.canEditAssetProfile) { if (this.canEditAssetProfile) {
@ -420,17 +439,20 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.assetProfileIdentifierForm.reset(); this.assetProfileIdentifierForm.reset();
} }
public onClose() { protected onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }
public onDeleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) { protected onDeleteProfileData({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
this.dialogRef.close(); this.dialogRef.close();
} }
public onGatherProfileDataBySymbol({ protected onGatherProfileDataBySymbol({
dataSource, dataSource,
symbol symbol
}: AssetProfileIdentifier) { }: AssetProfileIdentifier) {
@ -440,7 +462,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe(); .subscribe();
} }
public onGatherSymbol({ protected onGatherSymbol({
dataSource, dataSource,
range, range,
symbol symbol
@ -453,13 +475,13 @@ export class GfAssetProfileDialogComponent implements OnInit {
.subscribe(); .subscribe();
} }
public onMarketDataChanged(withRefresh: boolean = false) { protected onMarketDataChanged(withRefresh: boolean = false) {
if (withRefresh) { if (withRefresh) {
this.initialize(); this.initialize();
} }
} }
public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) { protected onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService this.dataService
.postBenchmark({ dataSource, symbol }) .postBenchmark({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -472,50 +494,48 @@ export class GfAssetProfileDialogComponent implements OnInit {
}); });
} }
public onSetEditAssetProfileIdentifierMode() { protected onSetEditAssetProfileIdentifierMode() {
this.isEditAssetProfileIdentifierMode = true; this.isEditAssetProfileIdentifierMode = true;
this.assetProfileForm.disable(); this.assetProfileForm.disable();
} }
public async onSubmitAssetProfileForm() { protected async onSubmitAssetProfileForm() {
let countries = []; let countries: Prisma.InputJsonArray = [];
let scraperConfiguration: ScraperConfiguration = { let scraperConfiguration: Prisma.InputJsonObject | undefined = {
selector: '', selector: '',
url: '' url: ''
}; };
let sectors = []; let sectors: Prisma.InputJsonArray = [];
let symbolMapping = {}; let symbolMapping: Record<string, string> = {};
try { try {
countries = JSON.parse(this.assetProfileForm.get('countries').value); countries = JSON.parse(
this.assetProfileForm.controls.countries.value ?? '[]'
) as Prisma.InputJsonArray;
} catch {} } catch {}
try { try {
scraperConfiguration = { scraperConfiguration = {
defaultMarketPrice: defaultMarketPrice:
(this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls
'defaultMarketPrice' .defaultMarketPrice?.value ?? undefined,
].value as number) || undefined,
headers: JSON.parse( headers: JSON.parse(
this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.headers
'headers' .value ?? '{}'
].value ) as Record<string, string>,
),
locale: locale:
this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.locale
'locale' ?.value ?? undefined,
].value || undefined, mode:
mode: this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.mode
'mode' ?.value ?? undefined,
].value as ScraperConfiguration['mode'],
selector: selector:
this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.selector
'selector' .value ?? '',
].value, url:
url: this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.url
'url' .value ?? ''
].value
}; };
if (!scraperConfiguration.selector || !scraperConfiguration.url) { if (!scraperConfiguration.selector || !scraperConfiguration.url) {
@ -536,28 +556,32 @@ export class GfAssetProfileDialogComponent implements OnInit {
} }
try { try {
sectors = JSON.parse(this.assetProfileForm.get('sectors').value); sectors = JSON.parse(
this.assetProfileForm.controls.sectors.value ?? '[]'
) as Prisma.InputJsonArray;
} catch {} } catch {}
try { try {
symbolMapping = JSON.parse( symbolMapping = JSON.parse(
this.assetProfileForm.get('symbolMapping').value this.assetProfileForm.controls.symbolMapping.value ?? '{}'
); ) as Record<string, string>;
} catch {} } catch {}
const assetProfile: UpdateAssetProfileDto = { const assetProfile: UpdateAssetProfileDto = {
countries, countries,
scraperConfiguration,
sectors, sectors,
symbolMapping, symbolMapping,
assetClass: this.assetProfileForm.get('assetClass').value, assetClass: this.assetProfileForm.controls.assetClass.value ?? undefined,
assetSubClass: this.assetProfileForm.get('assetSubClass').value, assetSubClass:
comment: this.assetProfileForm.get('comment').value || null, this.assetProfileForm.controls.assetSubClass.value ?? undefined,
currency: this.assetProfileForm.get('currency').value, comment: this.assetProfileForm.controls.comment.value ?? undefined,
isActive: this.assetProfileForm.get('isActive').value, currency: this.assetProfileForm.controls.currency.value ?? undefined,
name: this.assetProfileForm.get('name').value, isActive: isBoolean(this.assetProfileForm.controls.isActive.value)
scraperConfiguration: ? this.assetProfileForm.controls.isActive.value
scraperConfiguration as unknown as Prisma.InputJsonObject, : undefined,
url: this.assetProfileForm.get('url').value || null name: this.assetProfileForm.controls.name.value ?? undefined,
url: this.assetProfileForm.controls.url.value ?? undefined
}; };
try { try {
@ -600,7 +624,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
this.initialize(); this.initialize();
}, },
error: (error) => { error: (error: HttpErrorResponse) => {
console.error($localize`Could not save asset profile`, error); console.error($localize`Could not save asset profile`, error);
this.snackBar.open( this.snackBar.open(
@ -614,12 +638,14 @@ export class GfAssetProfileDialogComponent implements OnInit {
}); });
} }
public async onSubmitAssetProfileIdentifierForm() { protected async onSubmitAssetProfileIdentifierForm() {
const assetProfileIdentifier: UpdateAssetProfileDto = { const assetProfileIdentifier: UpdateAssetProfileDto = {
dataSource: this.assetProfileIdentifierForm.get('assetProfileIdentifier') dataSource:
.value.dataSource, this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value
symbol: this.assetProfileIdentifierForm.get('assetProfileIdentifier') ?.dataSource ?? undefined,
.value.symbol symbol:
this.assetProfileIdentifierForm.controls.assetProfileIdentifier.value
?.symbol ?? undefined
}; };
try { try {
@ -676,38 +702,33 @@ export class GfAssetProfileDialogComponent implements OnInit {
}); });
} }
public onTestMarketData() { protected onTestMarketData() {
this.adminService this.adminService
.testMarketData({ .testMarketData({
dataSource: this.data.dataSource, dataSource: this.data.dataSource,
scraperConfiguration: { scraperConfiguration: {
defaultMarketPrice: this.assetProfileForm.controls[ defaultMarketPrice:
'scraperConfiguration' this.assetProfileForm.controls.scraperConfiguration.controls
].controls['defaultMarketPrice'].value as number, .defaultMarketPrice?.value,
headers: JSON.parse( headers: JSON.parse(
this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.headers
'headers' .value ?? '{}'
].value ) as Record<string, string>,
),
locale: locale:
this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls.locale
'locale' ?.value || undefined,
].value || undefined, mode: this.assetProfileForm.controls.scraperConfiguration.controls
mode: this.assetProfileForm.controls['scraperConfiguration'].controls[ .mode?.value,
'mode'
].value,
selector: selector:
this.assetProfileForm.controls['scraperConfiguration'].controls[ this.assetProfileForm.controls.scraperConfiguration.controls
'selector' .selector.value,
].value, url: this.assetProfileForm.controls.scraperConfiguration.controls.url
url: this.assetProfileForm.controls['scraperConfiguration'].controls[ .value
'url'
].value
}, },
symbol: this.data.symbol symbol: this.data.symbol
}) })
.pipe( .pipe(
catchError(({ error }) => { catchError(({ error }: HttpErrorResponse) => {
this.notificationService.alert({ this.notificationService.alert({
message: error?.message, message: error?.message,
title: $localize`Error` title: $localize`Error`
@ -723,26 +744,26 @@ export class GfAssetProfileDialogComponent implements OnInit {
' ' + ' ' +
price + price +
' ' + ' ' +
this.assetProfileForm.get('currency').value this.assetProfileForm.controls.currency.value
}); });
}); });
} }
public onToggleIsActive({ checked }: MatCheckboxChange) { protected onToggleIsActive({ checked }: MatCheckboxChange) {
if (checked) { if (checked) {
this.assetProfileForm.get('isActive')?.setValue(true); this.assetProfileForm.controls.isActive.setValue(true);
} else { } else {
this.assetProfileForm.get('isActive')?.setValue(false); this.assetProfileForm.controls.isActive.setValue(false);
} }
if (checked === this.assetProfile.isActive) { if (checked === this.assetProfile.isActive) {
this.assetProfileForm.get('isActive')?.markAsPristine(); this.assetProfileForm.controls.isActive.markAsPristine();
} else { } else {
this.assetProfileForm.get('isActive')?.markAsDirty(); this.assetProfileForm.controls.isActive.markAsDirty();
} }
} }
public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) { protected onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService this.dataService
.deleteBenchmark({ dataSource, symbol }) .deleteBenchmark({ dataSource, symbol })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -755,15 +776,15 @@ export class GfAssetProfileDialogComponent implements OnInit {
}); });
} }
public onTriggerSubmitAssetProfileForm() { protected onTriggerSubmitAssetProfileForm() {
if (this.assetProfileForm.valid) { if (this.assetProfileForm.valid) {
this.onSubmitAssetProfileForm(); this.onSubmitAssetProfileForm();
} }
} }
private isNewSymbolValid(control: AbstractControl): ValidationErrors { private isNewSymbolValid(control: AbstractControl): ValidationErrors | null {
const currentAssetProfileIdentifier: AssetProfileIdentifier | undefined = const currentAssetProfileIdentifier: AssetProfileIdentifier | undefined =
control.get('assetProfileIdentifier').value; control.get('assetProfileIdentifier')?.value;
if ( if (
currentAssetProfileIdentifier?.dataSource === this.data?.dataSource && currentAssetProfileIdentifier?.dataSource === this.data?.dataSource &&
@ -773,5 +794,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
equalsPreviousProfileIdentifier: true equalsPreviousProfileIdentifier: true
}; };
} }
return null;
} }
} }

4
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -419,11 +419,11 @@
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Url</mat-label> <mat-label i18n>Url</mat-label>
<input formControlName="url" matInput type="text" /> <input formControlName="url" matInput type="text" />
@if (assetProfileForm.get('url').value) { @if (assetProfileForm.controls.url.value) {
<gf-entity-logo <gf-entity-logo
class="mr-3" class="mr-3"
matSuffix matSuffix
[url]="assetProfileForm.get('url').value" [url]="assetProfileForm.controls.url.value"
/> />
} }
</mat-form-field> </mat-form-field>

Loading…
Cancel
Save