From 59d2fe8c7b40866cd97f58cff2162d9a2aa0e905 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 23 Oct 2025 19:06:55 +0300 Subject: [PATCH] UI: Add bacis config RELATED_ENTITIES_AGGREGATION cf --- .../calculated-field.module.ts | 4 + ...ulated-field-argument-panel.component.html | 54 +++--- ...lculated-field-argument-panel.component.ts | 11 ++ ...calculated-field-arguments-table.module.ts | 9 +- ...d-aggregation-arguments-table.component.ts | 70 ++++++++ .../calculated-field-dialog.component.html | 7 + .../calculated-field-dialog.component.scss | 3 + .../calculated-field-output.component.html | 85 +++++----- .../calculated-field-output.component.ts | 13 +- ...ities-aggregation-component.component.html | 75 +++++++++ ...ntities-aggregation-component.component.ts | 154 ++++++++++++++++++ ...d-entities-aggregation-component.module.ts | 45 +++++ .../components/time-unit-input.component.html | 6 +- .../shared/models/calculated-field.models.ts | 24 ++- .../assets/locale/locale.constant-en_US.json | 15 +- 15 files changed, 505 insertions(+), 70 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts index e0db7a6eef..5e3a854aa2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -38,6 +38,9 @@ import { import { PropagationConfigurationModule } from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module'; +import { + RelatedEntitiesAggregationComponentModule +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module'; @NgModule({ declarations: [ @@ -52,6 +55,7 @@ import { EntityDebugSettingsButtonComponent, SimpleConfigurationModule, PropagationConfigurationModule, + RelatedEntitiesAggregationComponentModule, ], exports: [ CalculatedFieldDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html index 77bdedb068..607b107094 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -18,6 +18,11 @@
{{ 'calculated-fields.argument-settings' | translate }}
+ @if (hint) { +
+ {{ hint | translate }} +
+ }
@if (!isOutputKey) { } -
-
{{ 'entity.entity-type' | translate }}
- - - @for (type of argumentEntityTypes; track type) { - {{ ArgumentEntityTypeTranslations.get(type) | translate }} + @if (!hiddenEntityTypes) { +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + @if (argumentType.touched && argumentType.hasError('required')) { + + warning + } - - @if (argumentType.touched && argumentType.hasError('required')) { - - warning - - } - -
+
+
+ } @if (ArgumentEntityTypeParamsMap.has(entityType)) {
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
@@ -143,9 +150,18 @@ } @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
-
{{ 'calculated-fields.default-value' | translate }}
+
{{ 'calculated-fields.default-value' | translate }}
+ @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { + + warning + + }
} @else { 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 070ffb5c06..6f90d126e0 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 @@ -52,6 +52,7 @@ import { Store } from '@ngrx/store'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { TenantId } from '@shared/models/id/tenant-id'; +import { deduplicationStrategiesHintTranslations } from '@home/components/rule-node/rule-node-config.models'; @Component({ selector: 'tb-calculated-field-argument-panel', @@ -68,6 +69,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() isScript: boolean; @Input() usedArgumentNames: string[]; @Input() isOutputKey = false; + @Input() hiddenEntityTypes = false; + @Input() defaultValueRequired = false; + @Input() hint: string; @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @@ -146,6 +150,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.setInitialEntityType(); this.setWatchKeyChange(); + if (this.defaultValueRequired) { + this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); + this.argumentFormGroup.get('defaultValue').updateValueAndValidity({onlySelf: true}); + } + this.argumentTypes = Object.values(ArgumentType) .filter(type => type !== ArgumentType.Rolling || this.isScript); } @@ -311,4 +320,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.entityNameSubject.next(null); } } + + protected readonly deduplicationStrategiesHintTranslations = deduplicationStrategiesHintTranslations; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts index 082001f052..cae0b92387 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts @@ -26,6 +26,9 @@ import { import { PropagateArgumentsTableComponent } from '@home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component'; +import { + RelatedAggregationArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component'; @NgModule({ imports: [ @@ -35,11 +38,13 @@ import { declarations: [ CalculatedFieldArgumentPanelComponent, CalculatedFieldArgumentsTableComponent, - PropagateArgumentsTableComponent + PropagateArgumentsTableComponent, + RelatedAggregationArgumentsTableComponent ], exports: [ CalculatedFieldArgumentsTableComponent, - PropagateArgumentsTableComponent + PropagateArgumentsTableComponent, + RelatedAggregationArgumentsTableComponent ] }) export class CalculatedFieldArgumentsTableModule {} 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 new file mode 100644 index 0000000000..7c9212d3c2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, DestroyRef, forwardRef, 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'; +import { Store } from '@ngrx/store'; +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'; + +@Component({ + selector: 'tb-related-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), + multi: true + } + ], +}) +export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { + + constructor( + protected fb: FormBuilder, + protected popoverService: TbPopoverService, + protected viewContainerRef: ViewContainerRef, + protected cd: ChangeDetectorRef, + protected renderer: Renderer2, + protected entityService: EntityService, + protected destroyRef: DestroyRef, + protected store: Store + ) { + super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store); + + this.argumentNameColumn = 'calculated-fields.argument-name'; + this.displayColumns = ['name', 'type', 'key', 'actions']; + this.panelAdditionalCtx = { + hiddenEntityTypes: true, + defaultValueRequired: true, + argumentEntityTypes: [ArgumentEntityType.Current], + hint: 'calculated-fields.hint.setting-arguments-aggregation' + }; + + this.isScript = false; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 1d9dcc98f1..478d41f42b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -75,6 +75,13 @@ [testScript]="onTestScript.bind(this)"> } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + + } @default { @if (simpleMode) { -
- - - {{ - (outputForm.get('type').value === OutputType.Timeseries - ? 'calculated-fields.timeseries-key' - : 'calculated-fields.attribute-key') - | translate - }} - - - @if (outputForm.get('name').errors && outputForm.get('name').touched) { - - @if (outputForm.get('name').hasError('required')) { - {{ 'common.hint.key-required' | translate }} - } @else if (outputForm.get('name').hasError('pattern')) { - {{ 'common.hint.key-pattern' | translate }} - } @else if (outputForm.get('name').hasError('maxlength')) { - {{ 'common.hint.key-max-length' | translate }} - } - - } - - - {{ 'calculated-fields.decimals-by-default' | translate }} - - @if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { - {{ 'calculated-fields.hint.decimals-range' | translate }} - } - -
- - - - - - - - - + @if (hiddenName) { +
+ + {{ 'calculated-fields.decimals-by-default' | translate }} + + @if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { + {{ 'calculated-fields.hint.decimals-range' | translate }} + } + + +
+ } @else { +
+ + + {{ + (outputForm.get('type').value === OutputType.Timeseries + ? 'calculated-fields.timeseries-key' + : 'calculated-fields.attribute-key') + | translate + }} + + + @if (outputForm.get('name').errors && outputForm.get('name').touched) { + + @if (outputForm.get('name').hasError('required')) { + {{ 'common.hint.key-required' | translate }} + } @else if (outputForm.get('name').hasError('pattern')) { + {{ 'common.hint.key-pattern' | translate }} + } @else if (outputForm.get('name').hasError('maxlength')) { + {{ 'common.hint.key-max-length' | translate }} + } + + } + + + {{ 'calculated-fields.decimals-by-default' | translate }} + + @if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { + {{ 'calculated-fields.hint.decimals-range' | translate }} + } + +
+ + } }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 9a95c921fa..464ca99bd2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -35,6 +35,7 @@ import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType } from '@shared/models/entity-type.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-calculate-field-output', @@ -55,8 +56,13 @@ import { EntityType } from '@shared/models/entity-type.models'; export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @Input() + @coerceBoolean() simpleMode = false; + @Input() + @coerceBoolean() + hiddenName = false; + @Input({required: true}) entityId: EntityId; @@ -137,11 +143,14 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val } private updatedFormWithMode(): void { - if (this.simpleMode) { + if (this.simpleMode && !this.hiddenName) { this.outputForm.get('name').enable({emitEvent: false}); - this.outputForm.get('decimalsByDefault').enable({emitEvent: false}); } else { this.outputForm.get('name').disable({emitEvent: false}); + } + if (this.simpleMode) { + this.outputForm.get('decimalsByDefault').enable({emitEvent: false}); + } else { this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); } } 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 new file mode 100644 index 0000000000..1988141c2a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -0,0 +1,75 @@ + +
+
+
+ {{ 'calculated-fields.aggregation-path-related-entities' | translate }} +
+
+ + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
+
+
+
+ {{ 'calculated-fields.arguments' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.metrics' | translate }} +
+ + +
+ +
+ +
+ calculated-fields.use-latest-timestamp +
+
+
+
+
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 new file mode 100644 index 0000000000..d10372beea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + CalculatedFieldOutput, + CalculatedFieldRelatedAggregationConfiguration, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + OutputType, + PropagationDirectionTranslations +} from '@shared/models/calculated-field.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-related-entities-aggregation-component', + templateUrl: './related-entities-aggregation-component.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + } + ], +}) +export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + relatedAggregationConfiguration = this.fb.group({ + relation: this.fb.group({ + direction: [EntitySearchDirection.FROM, Validators.required], + relationType: ['Contains', Validators.required], + }), + arguments: this.fb.control({}), + deduplicationIntervalInSec: [], + output: this.fb.control({ + scope: AttributeScope.SERVER_SCOPE, + type: OutputType.Timeseries, + }), + 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; + + + functionArgs$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private propagateChange: (config: CalculatedFieldRelatedAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + + this.relatedAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => { + this.updatedModel(value); + }) + } + + validate(): ValidationErrors | null { + return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { + this.relatedAggregationConfiguration.patchValue(value, {emitEvent: false}); + setTimeout(() => { + this.relatedAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldRelatedAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.relatedAggregationConfiguration.disable({emitEvent: false}); + } else { + this.relatedAggregationConfiguration.enable({emitEvent: false}); + } + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldRelatedAggregationConfiguration): void { + value.type = CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + this.propagateChange(value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts new file mode 100644 index 0000000000..a2c48c1896 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts @@ -0,0 +1,45 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + RelatedEntitiesAggregationComponentComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + RelatedEntitiesAggregationComponentComponent, + ], + exports: [ + RelatedEntitiesAggregationComponentComponent, + ] +}) +export class RelatedEntitiesAggregationComponentModule { +} diff --git a/ui-ngx/src/app/shared/components/time-unit-input.component.html b/ui-ngx/src/app/shared/components/time-unit-input.component.html index d5f6319576..53a444895b 100644 --- a/ui-ngx/src/app/shared/components/time-unit-input.component.html +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.html @@ -18,7 +18,7 @@
+ subscriptSizing="dynamic"> @if (labelText && !inlineField) { {{ labelText }} } @@ -41,7 +41,9 @@ {{ hasError }} - @if (!inlineField) { 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 8294a776df..8ea6659bca 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -65,7 +65,8 @@ export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', GEOFENCING = 'GEOFENCING', - PROPAGATION = 'PROPAGATION' + PROPAGATION = 'PROPAGATION', + RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION' } export const CalculatedFieldTypeTranslations = new Map( @@ -74,6 +75,7 @@ export const CalculatedFieldTypeTranslations = new Map; + useLatestTs: boolean; output: CalculatedFieldSimpleOutput; } @@ -105,6 +109,15 @@ export interface CalculatedFieldGeofencingConfiguration { output: CalculatedFieldOutput; } +export interface CalculatedFieldRelatedAggregationConfiguration { + type: CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + relation: RelationPathLevel; + arguments: Record; + deduplicationIntervalInSec: number; + useLatestTs: boolean; + output: Omit; +} + interface BasePropagationConfiguration { type: CalculatedFieldType.PROPAGATION; direction: EntitySearchDirection; @@ -250,7 +263,7 @@ export interface CalculatedFieldGeofencing { export interface RefDynamicSourceConfiguration { type?: ArgumentEntityType.RelationQuery; - levels?: Array<{direction: EntitySearchDirection; relationType: string;}>; + levels?: Array; } export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { @@ -317,6 +330,11 @@ export interface CalculatedFieldArgumentValueBase { type: ArgumentType; } +export interface RelationPathLevel { + direction: EntitySearchDirection; + relationType: string; +} + export interface CalculatedFieldAttributeArgumentValue extends CalculatedFieldArgumentValueBase { ts: number; value: ValueType; 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 331f3cdb09..0333c5ed2e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1054,7 +1054,8 @@ "simple": "Simple", "script": "Script", "geofencing" : "Geofencing", - "propagation": "Propagation" + "propagation": "Propagation", + "related-entities-aggregation": "Related entities aggregation" }, "arguments": "Arguments", "decimals-by-default": "Decimals by default", @@ -1088,6 +1089,7 @@ "shared-attributes": "Shared attributes", "attribute-key": "Attribute key", "default-value": "Default value", + "default-value-required": "Default value is required.", "limit": "Max values", "time-window": "Time window", "customer-name": "Customer name", @@ -1156,6 +1158,11 @@ "data-propagate": "Data to propagate", "output-key": "Output key", "copy-output-key": "Copy output key", + "aggregation-path-related-entities": "Aggregation path to related entities", + "metrics": "Metrics", + "deduplication-interval": "Deduplication interval", + "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} second.", + "deduplication-interval-required": "Deduplication interval is required.", "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.", @@ -1202,7 +1209,11 @@ "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second.", "propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.", - "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data." + "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.", + "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.", + "arguments-aggregation": "Defines input parameters used for filtering and aggregation.", + "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", + "metrics": "Defines metrics aggregated based on the configured arguments." } }, "ai-models": {