Calculated adjustments
This commit is contained in:
parent
7d57cde6fa
commit
8c7c54b554
@ -258,8 +258,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
}
|
||||
|
||||
private importCalculatedField(): void {
|
||||
this.importExportService.importCalculatedField(this.entityId)
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
|
||||
this.importExportService.openCalculatedFieldImportDialog()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(calculatedField => this.getCalculatedFieldDialog(calculatedField, 'action.add')),
|
||||
filter(Boolean),
|
||||
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
|
||||
filter(Boolean),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(() => this.updateData());
|
||||
}
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@
|
||||
matTooltipPosition="above">
|
||||
<mat-icon
|
||||
[matBadgeHidden]="!(argument.refEntityKey.type === ArgumentType.Rolling
|
||||
&& calculatedFieldType === CalculatedFieldType.SIMPLE)"
|
||||
&& calculatedFieldType === CalculatedFieldType.SIMPLE) && !entityNameErrorSet.has(argument.refEntityId?.id)"
|
||||
matBadgeColor="warn"
|
||||
matBadgeSize="small"
|
||||
matBadge="*"
|
||||
|
||||
@ -58,6 +58,8 @@ import { MatSort } from '@angular/material/sort';
|
||||
import { getCurrentAuthState } from '@core/auth/auth.selectors';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { NEVER } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-calculated-field-arguments-table',
|
||||
@ -88,6 +90,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
||||
errorText = '';
|
||||
argumentsFormArray = this.fb.array<AbstractControl>([]);
|
||||
entityNameMap = new Map<string, string>();
|
||||
entityNameErrorSet = new Set<string>();
|
||||
sortOrder = { direction: 'asc', property: '' };
|
||||
dataSource = new CalculatedFieldArgumentDatasource();
|
||||
|
||||
@ -168,6 +171,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
||||
buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
|
||||
tenantId: this.tenantId,
|
||||
entityName: this.entityName,
|
||||
entityHasError: this.entityNameErrorSet.has(argument.refEntityId?.id),
|
||||
usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
|
||||
};
|
||||
this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
|
||||
@ -198,6 +202,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
||||
if (this.calculatedFieldType === CalculatedFieldType.SIMPLE
|
||||
&& this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) {
|
||||
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
|
||||
} else if (this.entityNameErrorSet.size) {
|
||||
this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
|
||||
} else if (!this.argumentsFormArray.controls.length) {
|
||||
this.errorText = 'calculated-fields.hint.arguments-empty';
|
||||
} else {
|
||||
@ -234,11 +240,18 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
|
||||
}
|
||||
|
||||
private updateEntityNameMap(value: CalculatedFieldArgumentValue[]): void {
|
||||
this.entityNameErrorSet.clear();
|
||||
value.forEach(({ refEntityId = {}}) => {
|
||||
if (refEntityId.id && !this.entityNameMap.has(refEntityId.id)) {
|
||||
const { id, entityType } = refEntityId as EntityId;
|
||||
this.entityService.getEntity(entityType as EntityType, id, { ignoreLoading: true, ignoreErrors: true })
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
this.entityNameErrorSet.add(id);
|
||||
return NEVER;
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(entity => this.entityNameMap.set(id, entity.name));
|
||||
}
|
||||
});
|
||||
|
||||
@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogComponent } from '@shared/components/dialog.component';
|
||||
import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
|
||||
import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models';
|
||||
import { EventTableComponent } from '@home/components/event/event-table.component';
|
||||
import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models';
|
||||
|
||||
@ -46,7 +46,7 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.eventsTable.entitiesTable.updateData();
|
||||
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = () => this.data.value.type === CalculatedFieldType.SCRIPT;
|
||||
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments)
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
<div class="fixed-title-width tb-required">{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}</div>
|
||||
<tb-entity-autocomplete
|
||||
class="flex flex-1"
|
||||
#entityAutocomplete
|
||||
formControlName="id"
|
||||
inlineField
|
||||
[placeholder]="'action.set' | translate"
|
||||
@ -165,7 +166,7 @@
|
||||
</div>
|
||||
} @else {
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width">{{ 'calculated-fields.time-window' | translate }}</div>
|
||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.time-window' | translate }}</div>
|
||||
<tb-timeinterval
|
||||
subscriptSizing="dynamic"
|
||||
appearance="outline"
|
||||
@ -175,7 +176,7 @@
|
||||
</div>
|
||||
@if (maxDataPointsPerRollingArg) {
|
||||
<div class="tb-form-row limit-field-row">
|
||||
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
|
||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.limit' | translate }}</div>
|
||||
<div class="limit-slider-container flex w-full flex-1 flex-row items-center justify-start">
|
||||
<mat-slider class="flex-1" min="1" max="{{maxDataPointsPerRollingArg}}">
|
||||
<input matSliderThumb formControlName="limit" [value]="argumentFormGroup.get('limit').value"/>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core';
|
||||
import { TbPopoverComponent } from '@shared/components/popover.component';
|
||||
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants';
|
||||
@ -41,13 +41,14 @@ import { MINUTE } from '@shared/models/time/time.models';
|
||||
import { getCurrentAuthState } from '@core/auth/auth.selectors';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-calculated-field-argument-panel',
|
||||
templateUrl: './calculated-field-argument-panel.component.html',
|
||||
styleUrls: ['./calculated-field-argument-panel.component.scss']
|
||||
})
|
||||
export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
||||
export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@Input() buttonTitle: string;
|
||||
@Input() index: number;
|
||||
@ -55,9 +56,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
||||
@Input() entityId: EntityId;
|
||||
@Input() tenantId: string;
|
||||
@Input() entityName: string;
|
||||
@Input() entityHasError: boolean;
|
||||
@Input() calculatedFieldType: CalculatedFieldType;
|
||||
@Input() usedArgumentNames: string[];
|
||||
|
||||
@ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
|
||||
|
||||
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
|
||||
|
||||
readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg;
|
||||
@ -75,8 +79,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
||||
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]],
|
||||
}),
|
||||
defaultValue: ['', [Validators.pattern(oneSpaceInsideRegex)]],
|
||||
limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }],
|
||||
timeWindow: [MINUTE * 15],
|
||||
limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }, [Validators.required, Validators.min(1), Validators.max(this.maxDataPointsPerRollingArg)]],
|
||||
timeWindow: [MINUTE * 15, [Validators.required]],
|
||||
});
|
||||
|
||||
argumentTypes: ArgumentType[];
|
||||
@ -136,6 +140,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
|
||||
.filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.entityHasError) {
|
||||
this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
saveArgument(): void {
|
||||
const { refEntityId, ...restConfig } = this.argumentFormGroup.value;
|
||||
const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue;
|
||||
|
||||
@ -89,7 +89,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
|
||||
{entityName: this.entityName}), [Validators.required, Validators.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],
|
||||
saveRelations: [false, []],
|
||||
saveAttributes: [true, []],
|
||||
saveCredentials: [true, []]
|
||||
saveCredentials: [true, []],
|
||||
saveCalculatedFields: [true, []]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -187,18 +187,8 @@ export class ImportExportService {
|
||||
});
|
||||
}
|
||||
|
||||
public importCalculatedField(entityId: EntityId): Observable<CalculatedField> {
|
||||
public openCalculatedFieldImportDialog(): Observable<CalculatedField> {
|
||||
return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe(
|
||||
mergeMap((calculatedField: CalculatedField) => {
|
||||
if (!this.validateImportedCalculatedField({ entityId, ...calculatedField })) {
|
||||
this.store.dispatch(new ActionNotificationShow(
|
||||
{message: this.translate.instant('calculated-fields.invalid-file-error'),
|
||||
type: 'error'}));
|
||||
throw new Error('Invalid calculated field file');
|
||||
} else {
|
||||
return this.calculatedFieldsService.saveCalculatedField(this.prepareImport({ entityId, ...calculatedField }));
|
||||
}
|
||||
}),
|
||||
catchError(() => of(null)),
|
||||
);
|
||||
}
|
||||
@ -989,16 +979,6 @@ export class ImportExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private validateImportedCalculatedField(calculatedField: CalculatedField): boolean {
|
||||
const { name, configuration, entityId } = calculatedField;
|
||||
return isNotEmptyStr(name)
|
||||
&& isDefined(configuration)
|
||||
&& isDefined(entityId?.id)
|
||||
&& !!Object.keys(configuration.arguments).length
|
||||
&& isDefined(configuration.expression)
|
||||
&& isDefined(configuration.output)
|
||||
}
|
||||
|
||||
private validateImportedImage(image: ImageExportData): boolean {
|
||||
return !(!isNotEmptyStr(image.data)
|
||||
|| !isNotEmptyStr(image.title)
|
||||
|
||||
@ -127,7 +127,7 @@ export const ArgumentTypeTranslations = new Map<ArgumentType, string>(
|
||||
export interface CalculatedFieldArgument {
|
||||
refEntityKey: RefEntityKey;
|
||||
defaultValue?: string;
|
||||
refEntityId?: RefEntityKey;
|
||||
refEntityId?: RefEntityId;
|
||||
limit?: number;
|
||||
timeWindow?: number;
|
||||
}
|
||||
@ -138,7 +138,7 @@ export interface RefEntityKey {
|
||||
scope?: AttributeScope;
|
||||
}
|
||||
|
||||
export interface RefEntityKey {
|
||||
export interface RefEntityId {
|
||||
entityType: ArgumentEntityType;
|
||||
id: string;
|
||||
}
|
||||
@ -563,12 +563,12 @@ export const getCalculatedFieldArgumentsHighlights = (
|
||||
regex: `\\b${key}\\b`,
|
||||
next: argumentsObj[key].refEntityKey.type === ArgumentType.Rolling
|
||||
? 'calculatedFieldRollingArgumentValue'
|
||||
: 'start'
|
||||
: 'no_regex'
|
||||
}));
|
||||
const calculatedFieldCtxArgumentsHighlightRules = {
|
||||
calculatedFieldCtxArgs: [
|
||||
dotOperatorHighlightRule,
|
||||
...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'start' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule),
|
||||
...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'no_regex' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule),
|
||||
endGroupHighlightRule
|
||||
]
|
||||
};
|
||||
|
||||
@ -1074,7 +1074,8 @@
|
||||
"argument-type-required": "Argument type is required.",
|
||||
"max-args": "Maximum number of arguments reached.",
|
||||
"decimals-range": "Decimals by default should be a number between 0 and 15.",
|
||||
"expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius."
|
||||
"expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.",
|
||||
"arguments-entity-not-found": "Argument target entity not found."
|
||||
}
|
||||
},
|
||||
"confirm-on-exit": {
|
||||
@ -5570,12 +5571,12 @@
|
||||
"max-calculated-fields": "Calculated fields per entity maximum number",
|
||||
"max-calculated-fields-range": "Calculated fields per entity maximum number can't be negative",
|
||||
"max-calculated-fields-required": "Calculated fields per entity maximum number is required",
|
||||
"max-data-points-per-rolling-arg": "Maximum data points number in rolling arguments",
|
||||
"max-data-points-per-rolling-arg-range": "Maximum data points number in rolling arguments can't be negative",
|
||||
"max-data-points-per-rolling-arg-required": "Maximum data points number in rolling arguments is required",
|
||||
"max-arguments-per-cf": "Arguments per calculated field maximum number",
|
||||
"max-arguments-per-cf-range": "Arguments per calculated field maximum number can't be negative",
|
||||
"max-arguments-per-cf-required": "Arguments per calculated field maximum number is required",
|
||||
"max-data-points-per-rolling-arg": "Max data points number in rolling arguments",
|
||||
"max-data-points-per-rolling-arg-range": "Max data points number in rolling arguments can't be negative",
|
||||
"max-data-points-per-rolling-arg-required": "Max data points number in rolling arguments is required",
|
||||
"max-arguments-per-cf": "Arguments per calculated field max number",
|
||||
"max-arguments-per-cf-range": "Arguments per calculated field max number can't be negative",
|
||||
"max-arguments-per-cf-required": "Arguments per calculated field max number is required",
|
||||
"max-state-size": "State maximum size in KB",
|
||||
"max-state-size-range": "State maximum size in KB can't be negative",
|
||||
"max-state-size-required": "State maximum size in KB is required",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user