Browse Source

UI: New calculated field entity aggregation

pull/14253/head
Vladyslav_Prykhodko 9 months ago
parent
commit
a394f4015a
  1. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
  2. 70
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  3. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  4. 9
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts
  5. 71
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts
  6. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  7. 131
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  8. 205
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  9. 49
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts
  10. 84
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html
  11. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts
  12. 20
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html
  13. 18
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  14. 27
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts
  15. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  16. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  17. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  18. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts
  19. 6
      ui-ngx/src/app/shared/components/time-unit-input.component.html
  20. 7
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  21. 50
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  22. 34
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
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,

70
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html

@ -74,25 +74,27 @@
}
</ng-container>
<ng-container [formGroup]="refEntityKeyFormGroup">
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-type' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
@for (type of argumentTypes; track type) {
<mat-option [value]="type">{{ ArgumentTypeTranslations.get(type) | translate }}</mat-option>
@if (!hiddenEntityKeyTypes) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-type' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
@for (type of argumentTypes; track type) {
<mat-option [value]="type">{{ ArgumentTypeTranslations.get(type) | translate }}</mat-option>
}
</mat-select>
@if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-type-required' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-select>
@if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-type-required' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
</mat-form-field>
</div>
}
@if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) {
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
<div class="tb-form-row">
@ -149,21 +151,23 @@
}"></ng-container>
}
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
<div class="tb-form-row">
<div class="fixed-title-width" [class.tb-required]="defaultValueRequired">{{ 'calculated-fields.default-value' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="defaultValue" placeholder="{{ 'action.set' | translate }}"/>
@if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.default-value-required' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
@if (!hiddenDefaultValue) {
<div class="tb-form-row">
<div class="fixed-title-width" [class.tb-required]="defaultValueRequired">{{ 'calculated-fields.default-value' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="defaultValue" placeholder="{{ 'action.set' | translate }}"/>
@if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.default-value-required' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
}
} @else {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.time-window' | translate }}</div>

2
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;

9
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 {}

71
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<AppState>
) {
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;
}
}

7
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -85,6 +85,13 @@
[tenantId]="data.tenantId"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[tenantId]="data.tenantId"
></tb-entity-aggregation-component>
}
@default {
<tb-simple-configuration formControlName="configuration"
[entityId]="data.entityId"

131
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html

@ -0,0 +1,131 @@
<!--
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.
-->
<div [formGroup]="entityAggregationConfiguration" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title">
{{ 'calculated-fields.arguments' | translate }}
</div>
<div class="tb-form-hint tb-primary-fill hint-container">
{{ 'calculated-fields.entity-aggregation.argument-hint' | translate }}
</div>
<tb-entity-aggregation-arguments-table formControlName="arguments"
[entityId]="entityId"
[tenantId]="tenantId"
[entityName]="entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.metrics' | translate }}">
{{ 'calculated-fields.metrics.metrics' | translate }}
</div>
<tb-calculated-field-metrics-table formControlName="metrics"
simpleMode
[arguments]="arguments$ | async"
></tb-calculated-field-metrics-table>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.aggregation-interval-hint' | translate }}">
{{ 'calculated-fields.entity-aggregation.aggregation-interval' | translate }}
</div>
<ng-container formGroupName="interval">
<div class="flex items-start gap-3 xs:flex-col xs:items-stretch">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>calculated-fields.aggregate-interval-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of AggIntervalTypes" [value]="type">
{{ AggIntervalTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-timezone-select
class="flex-1"
appearance="outline"
subscriptSizing="dynamic"
required
userTimezoneByDefault
formControlName="tz">
</tb-timezone-select>
</div>
@if (entityAggregationConfiguration.get('interval.type').value === AggIntervalType.CUSTOM) {
<tb-time-unit-input required
[minTime]="0"
sameWidthInputs
appearance="outline"
subscriptSizing="dynamic"
labelText="{{ 'calculated-fields.aggregate-interval-value' | translate }}"
requiredText="{{ 'calculated-fields.aggregate-interval-value-required' | translate }}"
formControlName="multiplier">
</tb-time-unit-input>
}
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide flex" formControlName="allowOffsetMillis">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.apply-offset-hint' | translate }}">
{{ 'calculated-fields.entity-aggregation.apply-offset' | translate }}
</div>
</mat-slide-toggle>
@if (entityAggregationConfiguration.get('interval.allowOffsetMillis').value) {
<tb-time-unit-input required
[minTime]="1"
sameWidthInputs
appearance="outline"
subscriptSizing="dynamic"
labelText="{{ 'calculated-fields.entity-aggregation.offset-value' | translate }}"
requiredText="{{ 'calculated-fields.entity-aggregation.offset-value-required' | translate }}"
formControlName="offsetMillis">
</tb-time-unit-input>
}
</div>
</ng-container>
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide flex" formControlName="allowWatermark">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.wait-delay-hint' | translate }}">
{{ 'calculated-fields.entity-aggregation.wait-delay' | translate }}
</div>
</mat-slide-toggle>
@if (entityAggregationConfiguration.get('allowWatermark').value) {
<ng-container formGroupName="watermark">
<tb-time-unit-input required
[minTime]="1"
sameWidthInputs
appearance="outline"
labelText="{{ 'calculated-fields.entity-aggregation.wait-duration' | translate }}"
hintText="{{ 'calculated-fields.entity-aggregation.wait-duration-hint' | translate }}"
requiredText="{{ 'calculated-fields.entity-aggregation.wait-duration-required' | translate }}"
formControlName="duration">
</tb-time-unit-input>
<tb-time-unit-input required
[minTime]="0"
[maxTime]="entityAggregationConfiguration.get('watermark.duration').value"
sameWidthInputs
appearance="outline"
labelText="{{ 'calculated-fields.entity-aggregation.check-interval' | translate }}"
hintText="{{ 'calculated-fields.entity-aggregation.check-interval-hint' | translate }}"
maxErrorText="{{ 'calculated-fields.entity-aggregation.check-interval-max' | translate }}"
requiredText="{{ 'calculated-fields.entity-aggregation.check-interval-required' | translate }}"
formControlName="checkInterval">
</tb-time-unit-input>
</ng-container>
}
</div>
</div>
<tb-calculate-field-output formControlName="output"
containerInputClass="grid items-start grid-cols-2 gap-3 xs:grid-cols-1"
simpleMode
hiddenName
disableType
[entityId]="entityId"></tb-calculate-field-output>
</div>

205
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<CalculatedFieldOutput>({
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});
}
}
}

49
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 {
}

84
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html

@ -77,48 +77,52 @@
</mat-form-field>
</div>
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="filterExpanded"
[disabled]="!metricForm.get('allowFilter').value">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-stretch justify-center" formControlName="allowFilter"
(click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.metrics.filter-hint' | translate }}">
{{ 'calculated-fields.metrics.filter' | translate }}
@if (!simpleMode) {
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="filterExpanded"
[disabled]="!metricForm.get('allowFilter').value">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-stretch justify-center" formControlName="allowFilter"
(click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.metrics.filter-hint' | translate }}">
{{ 'calculated-fields.metrics.filter' | translate }}
</div>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func required
formControlName="filter"
functionName="filter"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
[highlightRules]="highlightRules"
[editorCompleter]="editorCompleter"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/filter_expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func required
formControlName="filter"
functionName="filter"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
[highlightRules]="highlightRules"
[editorCompleter]="editorCompleter"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/filter_expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</div>
<ng-container formGroupName="input">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.metrics.value-source' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
@for (inputType of AggInputTypes; track inputType) {
<mat-option [value]="inputType">{{ AggInputTypeTranslations.get(inputType) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</div>
}
<ng-container formGroupName="input">
@if (!simpleMode) {
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.metrics.value-source' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
@for (inputType of AggInputTypes; track inputType) {
<mat-option [value]="inputType">{{ AggInputTypeTranslations.get(inputType) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
@if (this.metricForm.get('input.type').value === AggInputType.key) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-name' | translate }}</div>

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts → 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<string>;
@Input() simpleMode: boolean;
@Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules;

20
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html

@ -20,10 +20,16 @@
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="!w-1/5 xs:!w-full sm:!w-1/2">
<mat-header-cell mat-sort-header *matHeaderCellDef class="xs:!w-full sm:!w-1/2" [class]="{
'!w-1/5': !simpleMode,
'!w-1/2': simpleMode,
}">
<div tbTruncateWithTooltip>{{ 'calculated-fields.metrics.metric-name' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let metric" class="argument-name-cell w-1/5 xs:w-full sm:w-1/2">
<mat-cell *matCellDef="let metric" class="argument-name-cell xs:w-full sm:w-1/2" [class]="{
'w-1/5': !simpleMode,
'w-1/2': simpleMode,
}">
<div class="flex items-center">
<div tbTruncateWithTooltip class="flex-1">{{ metric.name }}</div>
<tb-copy-button class="copy-argument-name"
@ -35,10 +41,16 @@
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'function'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/5 xs:hidden lt-md:w-1/2">
<mat-header-cell mat-sort-header *matHeaderCellDef class="xs:hidden lt-md:w-1/2" [class]="{
'w-1/5': !simpleMode,
'w-1/2': simpleMode,
}">
{{ 'calculated-fields.metrics.aggregation' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let metric" class="w-1/5 xs:hidden lt-md:w-1/2">
<mat-cell *matCellDef="let metric" class="xs:hidden lt-md:w-1/2" [class]="{
'w-1/5': !simpleMode,
'w-1/2': simpleMode,
}">
<div tbTruncateWithTooltip>{{ AggFunctionTranslations.get(metric.function) | translate }}</div>
</mat-cell>
</ng-container>

18
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts → 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<string>;
@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,

27
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 {
}

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -17,7 +17,7 @@
-->
<div class="tb-form-panel" [formGroup]="outputForm">
<div class="tb-form-panel-title">{{ 'calculated-fields.output' | translate }}</div>
<div class="flex flex-col gap-3">
<div [class]=containerInputClass>
<div class="flex gap-3 xs:flex-col">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label>
@ -44,7 +44,7 @@
</div>
@if (simpleMode) {
@if (hiddenName) {
<div class="grid grid-cols-2 items-start gap-3 xs:grid-cols-1">
<div class="grid items-start gap-3 has-[.simpleMode]:grid-cols-2 xs:grid-cols-1">
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
<ng-content select=".simpleMode"></ng-content>
</div>

13
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<string, boolean | undefined | null> = '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});
}
}
}

1
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"
></tb-calculated-field-metrics-table>
<tb-time-unit-input required
sameWidthInputs
appearance="outline"
subscriptSizing="dynamic"
labelText="{{ 'calculated-fields.deduplication-interval' | translate }}"

10
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts

@ -27,11 +27,8 @@ import {
RelatedEntitiesAggregationComponentComponent
} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component';
import {
CalculatedFieldMetricsTableComponent
} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component';
import {
CalculatedFieldMetricsPanelComponent
} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component';
CalculatedFieldMetricsTableModule
} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module';
@NgModule({
imports: [
@ -39,11 +36,10 @@ import {
SharedModule,
CalculatedFieldOutputModule,
CalculatedFieldArgumentsTableModule,
CalculatedFieldMetricsTableModule,
],
declarations: [
RelatedEntitiesAggregationComponentComponent,
CalculatedFieldMetricsTableComponent,
CalculatedFieldMetricsPanelComponent
],
exports: [
RelatedEntitiesAggregationComponentComponent,

6
ui-ngx/src/app/shared/components/time-unit-input.component.html

@ -16,7 +16,7 @@
-->
<section [formGroup]="timeInputForm" class="flex gap-4">
<mat-form-field [class]="{'number': inlineField, 'max-w-66%': !inlineField, 'flex-full': !inlineField}"
<mat-form-field [class]="{'number': inlineField, 'max-w-66%': !inlineField && !sameWidthInputs, 'flex-full': !inlineField}"
[appearance]="inlineField ? 'outline' : appearance"
subscriptSizing="dynamic">
@if (labelText && !inlineField) {
@ -36,13 +36,13 @@
warning
</mat-icon>
}
<mat-hint *ngIf="(subscriptSizing === 'fixed') && !inlineField"></mat-hint>
<mat-hint *ngIf="(subscriptSizing === 'fixed') && !inlineField">{{ hintText }}</mat-hint>
<mat-error *ngIf="hasError && !inlineField">
{{ hasError }}
</mat-error>
</mat-form-field>
<mat-form-field [class.h-fit]="!inlineField"
[class.max-w-33%]="!inlineField"
[class.max-w-33%]="!inlineField && !sameWidthInputs"
[class.flex-full]="!inlineField"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">

7
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;

50
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<CalculatedFieldType, string>(
@ -77,6 +78,7 @@ export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, stri
[CalculatedFieldType.GEOFENCING, 'calculated-fields.type.geofencing'],
[CalculatedFieldType.PROPAGATION, 'calculated-fields.type.propagation'],
[CalculatedFieldType.RELATED_ENTITIES_AGGREGATION, 'calculated-fields.type.related-entities-aggregation'],
[CalculatedFieldType.ENTITY_AGGREGATION, 'calculated-fields.type.entity-aggregation'],
]
)
@ -85,7 +87,8 @@ export type CalculatedFieldConfiguration =
| CalculatedFieldScriptConfiguration
| CalculatedFieldGeofencingConfiguration
| CalculatedFieldPropagationConfiguration
| CalculatedFieldRelatedAggregationConfiguration;
| CalculatedFieldRelatedAggregationConfiguration
| CalculatedFieldEntityAggregationConfiguration;
export interface CalculatedFieldSimpleConfiguration {
type: CalculatedFieldType.SIMPLE;
@ -120,6 +123,20 @@ export interface CalculatedFieldRelatedAggregationConfiguration {
output: Omit<CalculatedFieldSimpleOutput, 'name'>;
}
export interface CalculatedFieldEntityAggregationConfiguration {
type: CalculatedFieldType.ENTITY_AGGREGATION;
arguments: Record<string, CalculatedFieldArgument>;
metrics: Record<string, CalculatedFieldAggMetric>;
interval: AggInterval;
watermark?: WatermarkConfig;
output: Omit<CalculatedFieldSimpleOutput, 'name'>;
}
export interface WatermarkConfig {
duration?: number;
checkInterval?: number;
}
interface BasePropagationConfiguration {
type: CalculatedFieldType.PROPAGATION;
relation: RelationPathLevel;
@ -270,6 +287,35 @@ export const AggFunctionTranslations = new Map<AggFunction, string>([
[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, string>(
[
[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;

34
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.",

Loading…
Cancel
Save