From 1c344b4cc0621e3cec91f0587ac350add2198f5d Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 29 Oct 2025 12:49:17 +0200 Subject: [PATCH] UI: Fixed cf relation aggregation key; Fixed minor bug in validation cf --- ...lculated-field-argument-panel.component.ts | 3 ++ ...d-aggregation-arguments-table.component.ts | 29 +++++++++++++++++-- .../calculated-field-dialog.component.ts | 11 ++++--- .../geofencing-configuration.component.ts | 5 ++-- .../propagation-configuration.component.ts | 5 ++-- ...ities-aggregation-component.component.html | 3 +- ...ntities-aggregation-component.component.ts | 24 +++++++-------- .../simple-configuration.component.ts | 2 +- .../entity-key-autocomplete.component.ts | 2 +- .../components/time-unit-input.component.ts | 2 +- .../shared/models/calculated-field.models.ts | 11 +++++++ 11 files changed, 71 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 6ac5bce5ec..a0c1904cc8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -71,6 +71,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() hiddenEntityTypes = false; @Input() defaultValueRequired = false; @Input() hint: string; + @Input() predefinedEntityFilter: EntityFilter; @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @@ -222,6 +223,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } if (!onInit) { this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); + } else { + entityFilter = this.predefinedEntityFilter; } this.entityFilter = entityFilter; this.cd.markForCheck(); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts index 7c9212d3c2..1ce5f798a5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts @@ -14,7 +14,15 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, DestroyRef, forwardRef, Renderer2, ViewContainerRef, } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + Renderer2, + ViewContainerRef, +} from '@angular/core'; import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; import { TbPopoverService } from '@shared/components/popover.service'; import { EntityService } from '@core/http/entity.service'; @@ -23,7 +31,9 @@ import { AppState } from '@core/core.state'; import { CalculatedFieldArgumentsTableComponent } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; -import { ArgumentEntityType } from '@shared/models/calculated-field.models'; +import { ArgumentEntityType, RelationPathLevel } from '@shared/models/calculated-field.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-related-aggregation-arguments-table', @@ -44,6 +54,21 @@ import { ArgumentEntityType } from '@shared/models/calculated-field.models'; }) export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { + @Input({required: true}) + set relation(value: RelationPathLevel) { + this.panelAdditionalCtx.predefinedEntityFilter = { + type: AliasFilterType.relationsQuery, + rootStateEntity: false, + rootEntity: this.entityId, + direction: value.direction, + filters: [{ + relationType: value.relationType, + entityTypes: [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT] + }], + maxLevel: 1, + }; + } + constructor( protected fb: FormBuilder, protected popoverService: TbPopoverService, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index ed0d9dd1c3..2d21a611da 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -30,7 +30,7 @@ import { } from '@shared/models/calculated-field.models'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models'; -import { switchMap } from 'rxjs/operators'; +import { pairwise, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; @@ -106,7 +106,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent { const calculatedFieldId = this.data.value?.id?.id; if (calculatedFieldId) { - return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) .pipe( switchMap(event => { const args = event?.arguments ? JSON.parse(event.arguments) : null; @@ -121,6 +121,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); } private observeIsLoading(): void { @@ -138,9 +139,11 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - if (type !== CalculatedFieldType.SIMPLE && type !== CalculatedFieldType.SCRIPT) { + ).subscribe(([prevType, nextType]) => { + if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || + ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false}); } }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts index 835a3628ff..0ed0e7eb28 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts @@ -31,6 +31,7 @@ import { CalculatedFieldOutput, CalculatedFieldType, getCalculatedFieldCurrentEntityFilter, + notEmptyObjectValidator, OutputType } from '@shared/models/calculated-field.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -76,7 +77,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V latitudeKeyName: [null, [Validators.required]], longitudeKeyName: [null, [Validators.required]], }), - zoneGroups: this.fb.control>({}), + zoneGroups: this.fb.control>({}, notEmptyObjectValidator()), scheduledUpdateEnabled: [true], scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], output: this.fb.control({scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries}) @@ -114,7 +115,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V } validate(): ValidationErrors | null { - return this.geofencingConfiguration.valid || this.geofencingConfiguration.status === "DISABLED" ? null : { geofencingConfigError: false }; + return this.geofencingConfiguration.valid || this.geofencingConfiguration.disabled ? null : { geofencingConfigError: false }; } writeValue(config: CalculatedFieldGeofencingConfiguration): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index 2fc1642e6f..27a0445bf3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -33,6 +33,7 @@ import { CalculatedFieldType, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, OutputType, PropagationDirectionTranslations, PropagationWithExpression @@ -74,7 +75,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, testScript: () => Observable; propagateConfiguration = this.fb.group({ - arguments: this.fb.control({}), + arguments: this.fb.control({}, notEmptyObjectValidator()), applyExpressionToResolvedArguments: [false], relation: this.fb.group({ direction: [EntitySearchDirection.TO, Validators.required], @@ -122,7 +123,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, } validate(): ValidationErrors | null { - return this.propagateConfiguration.valid || this.propagateConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; + return this.propagateConfiguration.valid || this.propagateConfiguration.disabled ? null : {invalidPropagateConfig: false}; } writeValue(value: PropagationWithExpression): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html index 7f18ff54f5..914226b777 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -47,7 +47,8 @@ + [entityName]="entityName" + [relation]="relatedAggregationConfiguration.get('relation').value"/>
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts index 475a771223..af21e0ebdf 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -32,6 +32,7 @@ import { CalculatedFieldType, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, OutputType, PropagationDirectionTranslations } from '@shared/models/calculated-field.models'; @@ -72,14 +73,21 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu @Input({required: true}) entityName: string; + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; + relatedAggregationConfiguration = this.fb.group({ relation: this.fb.group({ direction: [EntitySearchDirection.FROM, Validators.required], relationType: ['Contains', Validators.required], }), - arguments: this.fb.control({}), - metrics: this.fb.control({}), - deduplicationIntervalInSec: [], + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], output: this.fb.control({ scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries, @@ -87,14 +95,6 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu useLatestTs: [false] }); - readonly ScriptLanguage = ScriptLanguage; - readonly CalculatedFieldType = CalculatedFieldType; - readonly OutputType = OutputType; - readonly Directions = Object.values(EntitySearchDirection) as Array; - readonly PropagationDirectionTranslations = PropagationDirectionTranslations; - readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; - - arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( map(argumentsObj => Object.keys(argumentsObj)) ); @@ -120,7 +120,7 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu } validate(): ValidationErrors | null { - return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; + return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; } writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index 89b720bd7e..7b9f6be1c0 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -140,7 +140,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid } validate(): ValidationErrors | null { - return this.simpleConfiguration.valid || this.simpleConfiguration.status === "DISABLED" ? null : {invalidSimpleConfig: false}; + return this.simpleConfiguration.valid || this.simpleConfiguration.disabled ? null : {invalidSimpleConfig: false}; } writeValue(value: SimpeConfiguration): void { diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts index 9bc8d4ced8..9b656594bc 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts @@ -84,7 +84,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({ pageLink: { page: 0, pageSize: 100 }, entityFilter: this.entityFilter(), - }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType()); + }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true}); }), map(result => { this.cachedResult = result; diff --git a/ui-ngx/src/app/shared/components/time-unit-input.component.ts b/ui-ngx/src/app/shared/components/time-unit-input.component.ts index 35b64514c7..678d9e9f00 100644 --- a/ui-ngx/src/app/shared/components/time-unit-input.component.ts +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.ts @@ -200,7 +200,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } validate(): ValidationErrors | null { - return this.timeInputForm.valid ? null : { + return this.timeInputForm.disabled || this.timeInputForm.valid ? null : { timeInput: false }; } 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 52ee345ffb..0ef454ae1f 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -30,6 +30,7 @@ import { endGroupHighlightRule } from '@shared/models/ace/ace.models'; import { EntitySearchDirection } from '@shared/models/relation.models'; +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; interface BaseCalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { entityId: EntityId; @@ -847,3 +848,13 @@ export const calculatedFieldDefaultScript = 'return {\n' + ' "temperatureC": (temperatureF - 32) / 1.8\n' + '};' + +export function notEmptyObjectValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) { + return {emptyObject: true}; + } + return null; + }; +}