diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index cd9f0373db..60581249d2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -258,8 +258,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(calculatedField, 'action.add')), + filter(Boolean), + switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + filter(Boolean), + takeUntilDestroyed(this.destroyRef) + ) .subscribe(() => this.updateData()); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 162bd3aa1e..abb34cb502 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -97,7 +97,7 @@ matTooltipPosition="above"> ([]); entityNameMap = new Map(); + entityNameErrorSet = new Set(); 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)); } }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts index 8618a11990..880de7c281 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts @@ -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 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 { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 79f2b6ebbc..15286af377 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -82,6 +82,7 @@
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
} @else {
-
{{ 'calculated-fields.time-window' | translate }}
+
{{ 'calculated-fields.time-window' | translate }}
@if (maxDataPointsPerRollingArg) {
-
{{ 'calculated-fields.limit' | translate }}
+
{{ 'calculated-fields.limit' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 482851c59c..8aa61eb1a4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -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; diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts index 0c4b696ad1..2042b7fdb6 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-create.component.ts @@ -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, []] }); } diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 6109275d6d..2ad60f0139 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -187,18 +187,8 @@ export class ImportExportService { }); } - public importCalculatedField(entityId: EntityId): Observable { + public openCalculatedFieldImportDialog(): Observable { 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) diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index c898342c68..7e553fde5f 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -127,7 +127,7 @@ export const ArgumentTypeTranslations = new Map( 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 ] }; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 883acdbf21..295a8ac35e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -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",