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 5e3a854aa2..e9bf146bfd 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 @@ -41,6 +41,9 @@ import { import { RelatedEntitiesAggregationComponentModule } from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module'; +import { + EntityAggregationComponentModule +} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module'; @NgModule({ declarations: [ @@ -56,6 +59,7 @@ import { SimpleConfigurationModule, PropagationConfigurationModule, RelatedEntitiesAggregationComponentModule, + EntityAggregationComponentModule, ], 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 c7eeaa253b..459c9a4dbb 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 @@ -74,25 +74,27 @@ } -
-
{{ 'calculated-fields.argument-type' | translate }}
- - - @for (type of argumentTypes; track type) { - {{ ArgumentTypeTranslations.get(type) | translate }} + @if (!hiddenEntityKeyTypes) { +
+
{{ 'calculated-fields.argument-type' | translate }}
+ + + @for (type of argumentTypes; track type) { + {{ ArgumentTypeTranslations.get(type) | translate }} + } + + @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { + + warning + } - - @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { - - warning - - } - -
+
+
+ } @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
@@ -149,21 +151,23 @@ }"> } @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { -
-
{{ 'calculated-fields.default-value' | translate }}
- - - @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { - - warning - - } - -
+ @if (!hiddenDefaultValue) { +
+
{{ 'calculated-fields.default-value' | translate }}
+ + + @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { + + warning + + } + +
+ } } @else {
{{ 'calculated-fields.time-window' | translate }}
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 dcc54c94b3..e58145cf05 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 @@ -69,6 +69,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() usedArgumentNames: string[]; @Input() isOutputKey = false; @Input() hiddenEntityTypes = false; + @Input() hiddenEntityKeyTypes = false; + @Input() hiddenDefaultValue = false; @Input() defaultValueRequired = false; @Input() hint: string; @Input() predefinedEntityFilter: EntityFilter; 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 cae0b92387..494d6f0159 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 @@ -29,6 +29,9 @@ import { import { RelatedAggregationArgumentsTableComponent } from '@home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component'; +import { + EntityAggregationArgumentsTableComponent +} from '@home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component'; @NgModule({ imports: [ @@ -39,12 +42,14 @@ import { CalculatedFieldArgumentPanelComponent, CalculatedFieldArgumentsTableComponent, PropagateArgumentsTableComponent, - RelatedAggregationArgumentsTableComponent + RelatedAggregationArgumentsTableComponent, + EntityAggregationArgumentsTableComponent, ], exports: [ CalculatedFieldArgumentsTableComponent, PropagateArgumentsTableComponent, - RelatedAggregationArgumentsTableComponent + RelatedAggregationArgumentsTableComponent, + EntityAggregationArgumentsTableComponent, ] }) export class CalculatedFieldArgumentsTableModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts new file mode 100644 index 0000000000..6d9b746d59 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts @@ -0,0 +1,71 @@ +/// +/// 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-entity-aggregation-arguments-table', + templateUrl: './calculated-field-arguments-table.component.html', + styleUrls: [`calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), + multi: true + } + ], +}) +export class EntityAggregationArgumentsTableComponent 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, + argumentEntityTypes: [ArgumentEntityType.Current], + hint: 'calculated-fields.entity-aggregation.argument-setting-hint', + hiddenDefaultValue: true, + hiddenEntityKeyTypes: true, + }; + + 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 7e538a8dc7..b65ae4dc2f 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 @@ -85,6 +85,13 @@ [tenantId]="data.tenantId" > } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } @default { +
+
+
+ {{ 'calculated-fields.arguments' | translate }} +
+
+ {{ 'calculated-fields.entity-aggregation.argument-hint' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.metrics.metrics' | translate }} +
+ +
+
+
+ {{ 'calculated-fields.entity-aggregation.aggregation-interval' | translate }} +
+ +
+ + calculated-fields.aggregate-interval-type + + + {{ AggIntervalTypeTranslations.get(type) | translate }} + + + + + +
+ @if (entityAggregationConfiguration.get('interval.type').value === AggIntervalType.CUSTOM) { + + + } +
+ +
+ {{ 'calculated-fields.entity-aggregation.apply-offset' | translate }} +
+
+ @if (entityAggregationConfiguration.get('interval.allowOffsetMillis').value) { + + + } +
+
+
+ +
+ {{ 'calculated-fields.entity-aggregation.wait-delay' | translate }} +
+
+ @if (entityAggregationConfiguration.get('allowWatermark').value) { + + + + + + + } +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts new file mode 100644 index 0000000000..9f48187b55 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts @@ -0,0 +1,205 @@ +/// +/// 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 { + AggInterval, + AggIntervalType, + AggIntervalTypeTranslations, + CalculatedFieldEntityAggregationConfiguration, + CalculatedFieldOutput, + CalculatedFieldType, + notEmptyObjectValidator, + OutputType +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; +import { isDefinedAndNotNull } from '@core/utils'; + +interface CalculatedFieldEntityAggregationConfigurationValue extends CalculatedFieldEntityAggregationConfiguration { + interval: AggInterval & {allowOffsetMillis?: boolean}; + allowWatermark: boolean; +} + +@Component({ + selector: 'tb-entity-aggregation-component', + templateUrl: './entity-aggregation-component.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + } + ], +}) +export class EntityAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + + entityAggregationConfiguration = this.fb.group({ + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + interval: this.fb.group({ + type: [AggIntervalType.HOUR], + tz: ['', Validators.required], + multiplier: [HOUR/SECOND, Validators.required], + allowOffsetMillis: [false], + offsetMillis: [MINUTE/SECOND, Validators.required], + }), + allowWatermark: [false], + watermark: this.fb.group({ + duration: [6 * MINUTE / SECOND, Validators.required], + checkInterval: [MINUTE / SECOND, Validators.required], + }), + output: this.fb.control({ + type: OutputType.Timeseries, + }), + }); + + arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => Object.keys(argumentsObj)) + ); + + AggIntervalType = AggIntervalType; + AggIntervalTypes = Object.values(AggIntervalType) as AggIntervalType[]; + AggIntervalTypeTranslations = AggIntervalTypeTranslations; + + private propagateChange: (config: CalculatedFieldEntityAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder) { + + this.entityAggregationConfiguration.get('interval.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type: AggIntervalType) => { + this.checkAggIntervalType(type); + }); + + this.entityAggregationConfiguration.get('interval.allowOffsetMillis').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((allow: boolean) => { + this.checkIntervalDuration(allow); + }); + + this.entityAggregationConfiguration.get('allowWatermark').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((allow: boolean) => { + this.checkWatermark(allow); + }); + + this.entityAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { + this.updatedModel(value); + }); + } + + validate(): ValidationErrors | null { + return this.entityAggregationConfiguration.valid || this.entityAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldEntityAggregationConfiguration): void { + const data: CalculatedFieldEntityAggregationConfigurationValue = { + ...value, + allowWatermark: isDefinedAndNotNull(value.watermark), + interval: {...value.interval, allowOffsetMillis: isDefinedAndNotNull(value?.interval?.offsetMillis)} + } + this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetMillis').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + setTimeout(() => { + this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldEntityAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.entityAggregationConfiguration.disable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.enable({emitEvent: false}); + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetMillis').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + } + } + + private updatedModel(value: CalculatedFieldEntityAggregationConfigurationValue): void { + value.type = CalculatedFieldType.ENTITY_AGGREGATION; + if (!value.interval.allowOffsetMillis) { + delete value.interval.offsetMillis; + } + delete value.interval.offsetMillis; + if (!value.allowWatermark) { + delete value.watermark; + } + delete value.allowWatermark; + this.propagateChange(value); + } + + private checkAggIntervalType(type: AggIntervalType) { + if (type === AggIntervalType.CUSTOM) { + this.entityAggregationConfiguration.get('interval.multiplier').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('interval.multiplier').disable({emitEvent: false}); + } + } + + private checkIntervalDuration(allow: boolean) { + if (allow) { + this.entityAggregationConfiguration.get('interval.offsetMillis').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('interval.offsetMillis').disable({emitEvent: false}); + } + } + + private checkWatermark(allow: boolean) { + if (allow) { + this.entityAggregationConfiguration.get('watermark').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('watermark').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts new file mode 100644 index 0000000000..44bbbc7237 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts @@ -0,0 +1,49 @@ +/// +/// 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 { + EntityAggregationComponentComponent +} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component'; +import { + CalculatedFieldMetricsTableModule +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + CalculatedFieldMetricsTableModule, + ], + declarations: [ + EntityAggregationComponentComponent, + ], + exports: [ + EntityAggregationComponentComponent, + ] +}) +export class EntityAggregationComponentModule { +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html similarity index 73% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html index 8b29689b7f..d57a487dbd 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html @@ -77,48 +77,52 @@
-
- - - - -
- {{ 'calculated-fields.metrics.filter' | translate }} + @if (!simpleMode) { +
+ + + + +
+ {{ 'calculated-fields.metrics.filter' | translate }} +
+
+
+
+ + +
{{ 'api-usage.tbel' | translate }}
- - - - - -
{{ 'api-usage.tbel' | translate }} -
-
-
-
-
- -
-
{{ 'calculated-fields.metrics.value-source' | translate }}
- - - @for (inputType of AggInputTypes; track inputType) { - {{ AggInputTypeTranslations.get(inputType) | translate }} - } - - + + +
+ } + + @if (!simpleMode) { +
+
{{ 'calculated-fields.metrics.value-source' | translate }}
+ + + @for (inputType of AggInputTypes; track inputType) { + {{ AggInputTypeTranslations.get(inputType) | translate }} + } + + +
+ } @if (this.metricForm.get('input.type').value === AggInputType.key) {
{{ 'calculated-fields.argument-name' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts similarity index 99% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts index d21db8ece9..b06a49d60d 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -46,6 +46,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit { @Input() metric: CalculatedFieldAggMetricValue; @Input() usedNames: string[]; @Input() arguments: Array; + @Input() simpleMode: boolean; @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html similarity index 91% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html index 4e05a0bbd5..67cfdfc122 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -20,10 +20,16 @@ - +
{{ 'calculated-fields.metrics.metric-name' | translate }}
- +
{{ metric.name }}
- + {{ 'calculated-fields.metrics.aggregation' | translate }} - +
{{ AggFunctionTranslations.get(metric.function) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts similarity index 94% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts index 3689e016b2..22adf9a801 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -16,11 +16,13 @@ import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, DestroyRef, forwardRef, Input, + OnInit, Renderer2, ViewChild, ViewContainerRef, @@ -51,7 +53,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CalculatedFieldMetricsPanelComponent -} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component'; +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { AceHighlightRules } from '@shared/models/ace/ace.models'; @@ -72,11 +74,12 @@ import { AceHighlightRules } from '@shared/models/ace/ace.models'; } ], }) -export class CalculatedFieldMetricsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { +export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { @Input() arguments: Array; @Input() editorCompleter: TbEditorCompleter; @Input() highlightRules: AceHighlightRules; + @Input({transform: booleanAttribute}) simpleMode: boolean = false; @ViewChild(MatSort, { static: true }) sort: MatSort; @@ -85,7 +88,7 @@ export class CalculatedFieldMetricsTableComponent implements ControlValueAccesso sortOrder = { direction: 'asc' as SortDirection, property: '' }; dataSource = new CalculatedFieldMetricsDatasource(); - displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions'] + displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; readonly AggFunctionTranslations = AggFunctionTranslations; readonly AggInputTypeTranslations = AggInputTypeTranslations; @@ -109,6 +112,12 @@ export class CalculatedFieldMetricsTableComponent implements ControlValueAccesso }); } + ngOnInit() { + if (this.simpleMode) { + this.displayColumns = ['name', 'function', 'actions']; + } + } + ngAfterViewInit(): void { this.sort.sortChange.asObservable().pipe( takeUntilDestroyed(this.destroyRef) @@ -155,7 +164,8 @@ export class CalculatedFieldMetricsTableComponent implements ControlValueAccesso usedNames: this.metricsFormArray.value.map(({ name }) => name).filter(name => name !== metric.name), arguments: this.arguments, editorCompleter: this.editorCompleter, - highlightRules: this.highlightRules + highlightRules: this.highlightRules, + simpleMode: this.simpleMode, }; this.popoverComponent = this.popoverService.displayPopover({ trigger, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts new file mode 100644 index 0000000000..10d391a0a4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldMetricsTableComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; + + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + CalculatedFieldMetricsTableComponent, + CalculatedFieldMetricsPanelComponent + ], + exports: [ + CalculatedFieldMetricsTableComponent + ] +}) +export class CalculatedFieldMetricsTableModule { + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 4d8515ac7e..c33d033f7a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -17,7 +17,7 @@ -->
{{ 'calculated-fields.output' | translate }}
-
+
{{ 'calculated-fields.output-type' | translate }} @@ -44,7 +44,7 @@
@if (simpleMode) { @if (hiddenName) { -
+
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 2a9215b7cf..94224006a8 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 @@ -63,6 +63,13 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val @coerceBoolean() hiddenName = false; + @Input() + @coerceBoolean() + disableType = false; + + @Input() + containerInputClass: string | string[] | Record = 'flex flex-col gap-3'; + @Input({required: true}) entityId: EntityId; @@ -141,6 +148,9 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val if (this.simpleMode && 'name' in value) { value.name = value.name?.trim() ?? ''; } + if (this.disableType) { + value.type = this.outputForm.get('type').value; + } this.propagateChange(value); } @@ -163,5 +173,8 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val } else { this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); } + if (this.disableType) { + this.outputForm.get('type').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 index 914226b777..8a0361ee58 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 @@ -60,6 +60,7 @@ [editorCompleter]="argumentsEditorCompleter$ | async" >
- @if (labelText && !inlineField) { @@ -36,13 +36,13 @@ warning } - + {{ hintText }} {{ hasError }} 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 678d9e9f00..bcfdabd574 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 @@ -55,6 +55,9 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() labelText: string; + @Input() + hintText: string; + @Input() @coerceBoolean() required: boolean; @@ -86,6 +89,10 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @coerceBoolean() inlineField: boolean; + @Input() + @coerceBoolean() + sameWidthInputs: boolean = false; + timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; timeUnitTranslations = timeUnitTranslations; 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 0ef454ae1f..c792c2d06f 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -67,7 +67,8 @@ export enum CalculatedFieldType { SCRIPT = 'SCRIPT', GEOFENCING = 'GEOFENCING', PROPAGATION = 'PROPAGATION', - RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION' + RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION', + ENTITY_AGGREGATION = 'ENTITY_AGGREGATION', } export const CalculatedFieldTypeTranslations = new Map( @@ -77,6 +78,7 @@ export const CalculatedFieldTypeTranslations = new Map; } +export interface CalculatedFieldEntityAggregationConfiguration { + type: CalculatedFieldType.ENTITY_AGGREGATION; + arguments: Record; + metrics: Record; + interval: AggInterval; + watermark?: WatermarkConfig; + output: Omit; +} + +export interface WatermarkConfig { + duration?: number; + checkInterval?: number; +} + interface BasePropagationConfiguration { type: CalculatedFieldType.PROPAGATION; relation: RelationPathLevel; @@ -270,6 +287,35 @@ export const AggFunctionTranslations = new Map([ [AggFunction.COUNT_UNIQUE, 'calculated-fields.metrics.aggregation-type.count-unique'], ]) +export enum AggIntervalType { + HOUR = 'HOUR', + DAY = 'DAY', + WEEK = 'WEEK', + WEEK_SUN_SAT = 'WEEK_SUN_SAT', + MONTH = 'MONTH', + YEAR = 'YEAR', + CUSTOM = 'CUSTOM' +} + +export const AggIntervalTypeTranslations = new Map( + [ + [AggIntervalType.HOUR, 'calculated-fields.aggregate-period.hour'], + [AggIntervalType.DAY, 'calculated-fields.aggregate-period.day'], + [AggIntervalType.WEEK, 'calculated-fields.aggregate-period.week'], + [AggIntervalType.WEEK_SUN_SAT, 'calculated-fields.aggregate-period.week-sun-sat'], + [AggIntervalType.MONTH, 'calculated-fields.aggregate-period.month'], + [AggIntervalType.YEAR, 'calculated-fields.aggregate-period.year'], + [AggIntervalType.CUSTOM, 'calculated-fields.aggregate-period.custom'] + ] +); + +export interface AggInterval { + type: AggIntervalType; + tz: string; + offsetMillis?: number + multiplier?: number +} + export interface CalculatedFieldAggMetric { function: AggFunction; filter?: string; 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 ab5fd6a3d0..58b100490d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1057,7 +1057,8 @@ "script": "Script", "geofencing" : "Geofencing", "propagation": "Propagation", - "related-entities-aggregation": "Related entities aggregation" + "related-entities-aggregation": "Related entities aggregation", + "entity-aggregation": "Entity aggregation" }, "arguments": "Arguments", "decimals-by-default": "Decimals by default", @@ -1191,6 +1192,37 @@ "filter": "Filter", "filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." }, + "aggregate-interval-type": "Aggregate interval type", + "aggregate-interval-value": "Aggregate interval value", + "aggregate-interval-value-required": "Aggregate interval value is required", + "aggregate-period": { + "hour": "Hour", + "day": "Day", + "week": "Week (Mon - Sun)", + "week-sun-sat": "Week (Sun - Sat)", + "month": "Month", + "year": "Year", + "custom": "Custom" + }, + "entity-aggregation": { + "argument-hint": "Data will be fetched from selected entity", + "argument-setting-hint": "Latest telemetry is the only available argument type for this calculated field", + "aggregation-interval": "Aggregation interval", + "aggregation-interval-hint": "Defines how often to perform aggregation. Example: every 1 hour aggregates data at 00:00, 01:00, 02:00, etc.", + "apply-offset": "Apply offset to aggregation interval", + "apply-offset-hint": "Defines how much to shift the start of each aggregation period (e.g., +10 minutes - 00:10, 01:10).", + "offset-value": "Offset value", + "offset-value-required": "Offset value is required", + "wait-delay": "Wait for delayed telemetry", + "wait-delay-hint": "Waits for delayed telemetry after the interval ends.", + "wait-duration": "Duration", + "wait-duration-required": "Duration is required", + "wait-duration-hint": "Defines how long to wait for delayed data after the interval ends.", + "check-interval": "Check for telemetry every", + "check-interval-required": "Check for telemetry every is required", + "check-interval-max": "Check interval need be less than the duration", + "check-interval-hint": "Defines how often to recheck for late telemetry during the watermark period." + }, "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.",