Browse Source

UI: Refactoring calculate fields component to support new type

pull/14107/head
Vladyslav_Prykhodko 11 months ago
parent
commit
4731e5bdbe
  1. 64
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
  2. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  3. 195
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  4. 226
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  5. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  6. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss
  7. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  8. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  9. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss
  10. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  11. 68
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html
  12. 157
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  13. 52
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts
  14. 36
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/caclculate-field-output.module.ts
  15. 86
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  16. 148
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  17. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts
  18. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component.html
  19. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component.scss
  20. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component.ts
  21. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component.html
  22. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component.scss
  23. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component.ts
  24. 98
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  25. 205
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  26. 48
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts
  27. 41
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  28. 4
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts
  29. 4
      ui-ngx/src/app/modules/home/pages/asset/asset.module.ts
  30. 2
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts
  31. 2
      ui-ngx/src/app/modules/home/pages/device/device.module.ts
  32. 2
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  33. 55
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  34. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

64
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts

@ -0,0 +1,64 @@
///
/// 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 { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import {
CalculatedFieldDialogComponent
} from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component';
import {
CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component';
import {
CalculatedFieldScriptTestDialogComponent
} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component';
import {
CalculatedFieldTestArgumentsComponent
} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component';
import {
EntityDebugSettingsButtonComponent
} from '@home/components/entity/debug/entity-debug-settings-button.component';
import { HomeComponentsModule } from '@home/components/home-components.module';
import {
GeofencingConfigurationModule
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module';
import {
SimpleConfigurationModule
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module';
@NgModule({
declarations: [
CalculatedFieldsTableComponent,
CalculatedFieldDialogComponent,
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
],
imports: [
CommonModule,
SharedModule,
GeofencingConfigurationModule,
EntityDebugSettingsButtonComponent,
HomeComponentsModule,
SimpleConfigurationModule
],
exports: [
CalculatedFieldsTableComponent,
]
})
export class CalculatedFieldsModule {}

8
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -158,9 +158,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private getExpressionLabel(entity: CalculatedField): string {
if (entity.type === CalculatedFieldType.SCRIPT) {
return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')';
} else {
return entity.configuration?.expression ?? '';
} else if (entity.type === CalculatedFieldType.SIMPLE) {
return entity.configuration.expression ?? '';
}
return '';
}
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
@ -287,6 +288,9 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> {
if (calculatedField.type === CalculatedFieldType.GEOFENCING || calculatedField.type === CalculatedFieldType.SIMPLE) {
return of(null);
}
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type;
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key)

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

@ -62,187 +62,22 @@
</mat-select>
</mat-form-field>
</div>
<ng-container [formGroup]="configFormGroup">
@if (fieldFormGroup.get('type').value !== CalculatedFieldType.GEOFENCING) {
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="data.entityId"
[tenantId]="data.tenantId"
[entityName]="data.entityName"
[calculatedFieldType]="fieldFormGroup.get('type').value"/>
</div>
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">
{{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }}
</div>
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="fieldFormGroup.get('type').value !== CalculatedFieldType.SIMPLE">
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'" required>
<div matSuffix
class="pr-2"
[tb-help-popup]="'math/math-methods_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) {
<mat-error>
@if (configFormGroup.get('expressionSIMPLE').hasError('required')) {
{{ 'calculated-fields.hint.expression-required' | translate }}
} @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) {
{{ 'calculated-fields.hint.expression-invalid' | translate }}
} @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) {
{{ 'calculated-fields.hint.expression-max-length' | translate }}
}
</mat-error>
} @else {
<mat-hint>{{ 'calculated-fields.hint.expression' | translate }}</mat-hint>
}
</mat-form-field>
<div [class.hidden]="fieldFormGroup.get('type').value !== CalculatedFieldType.SCRIPT">
<tb-js-func required
formControlName="expressionSCRIPT"
functionName="calculate"
[functionArgs]="functionArgs$ | async"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
[highlightRules]="argumentsHighlightRules$ | async"
[editorCompleter]="argumentsEditorCompleter$ | async"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/expression_fn">
<div toolbarPrefixButton class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="configFormGroup.get('arguments').invalid"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="configFormGroup.get('arguments').invalid">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</div>
</div>
} @else {
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.entity-coordinates' | translate }}">
{{ 'calculated-fields.entity-coordinates' | translate }}
</div>
<div class="flex items-start gap-3" [formGroup]="coordinatesFormGroup">
<tb-entity-key-autocomplete class="flex-1"
placeholder="{{ 'calculated-fields.latitude-time-series-key' | translate }}"
requiredText="{{ 'calculated-fields.latitude-time-series-key-required' | translate }}"
formControlName="latitudeKeyName"
[dataKeyType]="DataKeyType.timeseries"
[entityFilter]="currentEntityFilter"/>
<tb-entity-key-autocomplete class="flex-1"
placeholder="{{ 'calculated-fields.longitude-time-series-key' | translate }}"
requiredText="{{ 'calculated-fields.longitude-time-series-key-required' | translate }}"
formControlName="longitudeKeyName"
[dataKeyType]="DataKeyType.timeseries"
[entityFilter]="currentEntityFilter"/>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.geofencing-zone-groups' | translate }}">
{{ 'calculated-fields.geofencing-zone-groups' | translate }}
</div>
<tb-calculated-field-geofencing-zone-groups-table formControlName="zoneGroups"
[entityId]="data.entityId"
[tenantId]="data.tenantId"
[entityName]="data.entityName"/>
<div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity">
<mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled">
<div tb-hint-tooltip-icon="{{'calculated-fields.hint.zone-group-refresh-interval' | translate}}">
{{ 'calculated-fields.zone-group-refresh-interval' | translate }}
</div>
</mat-slide-toggle>
<div class="flex flex-row items-center justify-start gap-2">
<tb-time-unit-input required
inlineField
requiredText="{{ 'calculated-fields.hint.zone-group-refresh-interval-required' | translate }}"
minErrorText="{{ 'calculated-fields.hint.zone-group-refresh-interval-min' | translate: {min: minAllowedScheduledUpdateIntervalInSecForCF} }}"
[minTime]="minAllowedScheduledUpdateIntervalInSecForCF"
formControlName="scheduledUpdateInterval">
</tb-time-unit-input>
</div>
</div>
</div>
@switch (fieldFormGroup.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration" [entityId]="data.entityId" [entityName]="data.entityName" [tenantId]="data.tenantId">
</tb-geofencing-configuration>
}
<div class="tb-form-panel" [formGroup]="outputFormGroup">
<div class="tb-form-panel-title">{{ 'calculated-fields.output' | translate }}</div>
<div class="flex items-center gap-3">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label>
<mat-select formControlName="type">
@for (type of outputTypes; track type) {
<mat-option [value]="type">{{ OutputTypeTranslations.get(type) | translate}}</mat-option>
}
</mat-select>
</mat-form-field>
@if (outputFormGroup.get('type').value === OutputType.Attribute
&& (data.entityId.entityType === EntityType.DEVICE || data.entityId.entityType === EntityType.DEVICE_PROFILE)) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.attribute-scope' | translate }}</mat-label>
<mat-select formControlName="scope" class="w-full">
<mat-option [value]="AttributeScope.SERVER_SCOPE">
{{ 'calculated-fields.server-attributes' | translate }}
</mat-option>
<mat-option [value]="AttributeScope.SHARED_SCOPE">
{{ 'calculated-fields.shared-attributes' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
}
</div>
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) {
<div class="flex items-start gap-3">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>
{{ (outputFormGroup.get('type').value === OutputType.Timeseries
? 'calculated-fields.timeseries-key'
: 'calculated-fields.attribute-key')
| translate }}
</mat-label>
<input matInput formControlName="name" required>
@if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) {
<mat-error>
@if (outputFormGroup.get('name').hasError('required')) {
{{ 'common.hint.key-required' | translate }}
} @else if (outputFormGroup.get('name').hasError('pattern')) {
{{ 'common.hint.key-pattern' | translate }}
} @else if (outputFormGroup.get('name').hasError('maxlength')) {
{{ 'common.hint.key-max-length' | translate }}
}
</mat-error>
}
</mat-form-field>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.decimals-by-default' | translate }}</mat-label>
<input matInput type="number" formControlName="decimalsByDefault">
@if (outputFormGroup.get('decimalsByDefault').errors && outputFormGroup.get('decimalsByDefault').touched) {
<mat-error>{{ 'calculated-fields.hint.decimals-range' | translate }}</mat-error>
}
</mat-form-field>
</div>
<div class="tb-form-row" [formGroup]="configFormGroup" *ngIf="outputFormGroup.get('type').value === OutputType.Timeseries">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate>
calculated-fields.use-latest-timestamp
</div>
</mat-slide-toggle>
</div>
}
</div>
</ng-container>
@default {
<tb-simple-configuration formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[tenantId]="data.tenantId"
[isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT"
[testScript$]="onTestScript.bind(this)"
>
</tb-simple-configuration>
}
}
</div>
</div>
<div mat-dialog-actions class="justify-end">

226
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -18,36 +18,25 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import {
ArgumentEntityType,
CalculatedField,
CalculatedFieldConfiguration,
calculatedFieldDefaultScript,
CalculatedFieldGeofencing,
CalculatedFieldTestScriptFn,
CalculatedFieldType,
CalculatedFieldTypeTranslations,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
getCalculatedFieldCurrentEntityFilter,
OutputType,
OutputTypeTranslations
CalculatedFieldTypeTranslations
} from '@shared/models/calculated-field.models';
import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { EntityType } from '@shared/models/entity-type.models';
import { map, startWith, switchMap } from 'rxjs/operators';
import { switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs';
import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { EntityFilter } from '@shared/models/query/query.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { deepTrim } from '@core/utils';
export interface CalculatedFieldDialogData {
value?: CalculatedField;
@ -68,70 +57,22 @@ export interface CalculatedFieldDialogData {
})
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> {
readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF;
fieldFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
type: [CalculatedFieldType.SIMPLE],
debugSettings: [],
configuration: this.fb.group({
entityCoordinates: this.fb.group({
latitudeKeyName: [null, [Validators.required]],
longitudeKeyName: [null, [Validators.required]],
}),
arguments: this.fb.control({}),
zoneGroups: this.fb.control({}),
scheduledUpdateEnabled: [true],
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF],
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
expressionSCRIPT: [calculatedFieldDefaultScript],
output: this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }],
type: [OutputType.Timeseries],
decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]],
}),
useLatestTs: [false]
}),
configuration: this.fb.control<CalculatedFieldConfiguration>({} as CalculatedFieldConfiguration),
});
functionArgs$ = this.configFormGroup.get('arguments').valueChanges
.pipe(
startWith(this.data.value?.configuration?.arguments ?? {}),
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)])
);
argumentsEditorCompleter$ = this.configFormGroup.get('arguments').valueChanges
.pipe(
startWith(this.data.value?.configuration?.arguments ?? {}),
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj))
);
argumentsHighlightRules$ = this.configFormGroup.get('arguments').valueChanges
.pipe(
startWith(this.data.value?.configuration?.arguments ?? {}),
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj))
);
additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig,
action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null;
currentEntityFilter: EntityFilter;
isRelatedEntity: boolean;
readonly OutputTypeTranslations = OutputTypeTranslations;
readonly OutputType = OutputType;
readonly AttributeScope = AttributeScope;
readonly EntityType = EntityType;
readonly CalculatedFieldType = CalculatedFieldType;
readonly ScriptLanguage = ScriptLanguage;
readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[];
readonly outputTypes = Object.values(OutputType) as OutputType[];
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
readonly DataKeyType = DataKeyType;
constructor(protected store: Store<AppState>,
protected router: Router,
@ -143,48 +84,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
super(store, router, dialogRef);
this.observeIsLoading();
this.applyDialogData();
this.observeTypeChanges();
this.observeZoneChanges();
this.observeScheduledUpdateEnabled();
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.data.entityName, this.data.entityId);
}
get configFormGroup(): FormGroup {
return this.fieldFormGroup.get('configuration') as FormGroup;
}
get outputFormGroup(): FormGroup {
return this.fieldFormGroup.get('configuration').get('output') as FormGroup;
}
get coordinatesFormGroup(): FormGroup {
return this.fieldFormGroup.get('configuration').get('entityCoordinates') as FormGroup;
}
get fromGroupValue(): CalculatedField {
const { configuration, type, name, ...rest } = this.fieldFormGroup.value;
const { expressionSIMPLE, expressionSCRIPT, output, ...restConfig } = configuration;
let cf: CalculatedField = {
name: name.trim(),
type,
...rest
} as CalculatedField;
if (type !== CalculatedFieldType.GEOFENCING) {
cf.configuration = {
...restConfig,
type,
expression: configuration['expression'+type].trim(),
output: { ...output, name: output.name?.trim() ?? '' }
} as CalculatedFieldConfiguration;
} else {
cf.configuration = {
...restConfig,
type,
output: { ...output, name: output.name?.trim() ?? '' }
} as CalculatedFieldConfiguration;
delete cf.configuration.arguments;
}
return cf;
return deepTrim(this.fieldFormGroup.value as CalculatedField);
}
cancel(): void {
@ -199,12 +102,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}
}
onTestScript(): void {
onTestScript(): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id;
let testScriptDialogResult$: Observable<string>;
if (calculatedFieldId) {
testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId)
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId)
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
@ -212,114 +113,13 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}),
takeUntilDestroyed(this.destroyRef)
)
} else {
testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
}
testScriptDialogResult$.subscribe(expression => {
this.configFormGroup.get('expressionSCRIPT').setValue(expression);
this.configFormGroup.get('expressionSCRIPT').markAsDirty();
});
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
}
private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration;
const updatedConfig = { ...restConfig , ['expression'+type]: expression };
this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, debugSettings, ...value }, {emitEvent: false});
}
private observeTypeChanges(): void {
this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value);
this.toggleScopeByOutputType(this.outputFormGroup.get('type').value);
this.outputFormGroup.get('type').valueChanges
.pipe(takeUntilDestroyed())
.subscribe(type => this.toggleScopeByOutputType(type));
this.fieldFormGroup.get('type').valueChanges
.pipe(takeUntilDestroyed())
.subscribe(type => this.toggleKeyByCalculatedFieldType(type));
}
private observeZoneChanges(): void {
this.configFormGroup.get('zoneGroups').valueChanges
.pipe(takeUntilDestroyed())
.subscribe((zoneGroups: CalculatedFieldGeofencing) =>
this.checkRelatedEntity(zoneGroups)
);
this.checkRelatedEntity(this.configFormGroup.get('zoneGroups').value);
}
private observeScheduledUpdateEnabled(): void {
this.configFormGroup.get('scheduledUpdateEnabled').valueChanges
.pipe(takeUntilDestroyed())
.subscribe((value: boolean) =>
this.checkScheduledUpdateEnabled(value)
);
this.checkScheduledUpdateEnabled(this.configFormGroup.get('scheduledUpdateEnabled').value);
}
private checkScheduledUpdateEnabled(value: boolean) {
if (value) {
this.configFormGroup.get('scheduledUpdateInterval').enable({emitEvent: false});
} else {
this.configFormGroup.get('scheduledUpdateInterval').disable({emitEvent: false});
}
}
private checkRelatedEntity(zoneGroups: CalculatedFieldGeofencing) {
this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery);
}
private toggleScopeByOutputType(type: OutputType): void {
if (type === OutputType.Attribute) {
this.outputFormGroup.get('scope').enable({emitEvent: false});
} else {
this.outputFormGroup.get('scope').disable({emitEvent: false});
}
if (this.fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) {
if (type === OutputType.Attribute) {
this.configFormGroup.get('useLatestTs').disable({emitEvent: false});
} else {
this.configFormGroup.get('useLatestTs').enable({emitEvent: false});
}
} else {
this.configFormGroup.get('useLatestTs').disable({emitEvent: false});
}
}
private toggleKeyByCalculatedFieldType(type: CalculatedFieldType): void {
if (type === CalculatedFieldType.GEOFENCING) {
this.configFormGroup.get('entityCoordinates').enable({emitEvent: false});
this.configFormGroup.get('zoneGroups').enable({emitEvent: false});
this.configFormGroup.get('scheduledUpdateInterval').enable({emitEvent: false});
this.outputFormGroup.get('name').disable({emitEvent: false});
this.configFormGroup.get('useLatestTs').disable({emitEvent: false});
this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false});
this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false});
this.configFormGroup.get('arguments').disable({emitEvent: false});
} else {
this.configFormGroup.get('entityCoordinates').disable({emitEvent: false});
this.configFormGroup.get('zoneGroups').disable({emitEvent: false});
this.configFormGroup.get('scheduledUpdateInterval').disable({emitEvent: false});
if (type === CalculatedFieldType.SIMPLE) {
this.outputFormGroup.get('name').enable({emitEvent: false});
this.configFormGroup.get('expressionSIMPLE').enable({emitEvent: false});
this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false});
if (this.outputFormGroup.get('type').value === OutputType.Attribute) {
this.configFormGroup.get('useLatestTs').disable({emitEvent: false});
} else {
this.configFormGroup.get('useLatestTs').enable({emitEvent: false});
}
} else {
this.outputFormGroup.get('name').disable({emitEvent: false});
this.configFormGroup.get('useLatestTs').disable({emitEvent: false});
this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false});
this.configFormGroup.get('expressionSCRIPT').enable({emitEvent: false});
}
}
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
}
private observeIsLoading(): void {
@ -328,8 +128,6 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
this.fieldFormGroup.disable({emitEvent: false});
} else {
this.fieldFormGroup.enable({emitEvent: false});
this.toggleScopeByOutputType(this.outputFormGroup.get('type').value);
this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value);
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss

8
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts

@ -58,7 +58,7 @@ import { NULL_UUID } from '@shared/models/id/has-uuid';
import { BaseData } from '@shared/models/base-data';
import {
CalculatedFieldGeofencingZoneGroupsPanelComponent
} from '@home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component';
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component';
@Component({
selector: 'tb-calculated-field-geofencing-zone-groups-table',
@ -79,9 +79,9 @@ import {
})
export class CalculatedFieldGeofencingZoneGroupsTableComponent implements ControlValueAccessor, Validator, AfterViewInit {
@Input() entityId: EntityId;
@Input() tenantId: string;
@Input() entityName: string;
@Input({required: true}) entityId: EntityId;
@Input({required: true}) tenantId: string;
@Input({required: true}) entityName: string;
@ViewChild(MatSort, { static: true }) sort: MatSort;

68
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html

@ -0,0 +1,68 @@
<!--
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]="geofencingConfiguration" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.entity-coordinates' | translate }}">
{{ 'calculated-fields.entity-coordinates' | translate }}
</div>
<div class="flex items-start gap-3" formGroupName="entityCoordinates">
<tb-entity-key-autocomplete class="flex-1"
placeholder="{{ 'calculated-fields.latitude-time-series-key' | translate }}"
requiredText="{{ 'calculated-fields.latitude-time-series-key-required' | translate }}"
formControlName="latitudeKeyName"
[dataKeyType]="DataKeyType.timeseries"
[entityFilter]="currentEntityFilter"/>
<tb-entity-key-autocomplete class="flex-1"
placeholder="{{ 'calculated-fields.longitude-time-series-key' | translate }}"
requiredText="{{ 'calculated-fields.longitude-time-series-key-required' | translate }}"
formControlName="longitudeKeyName"
[dataKeyType]="DataKeyType.timeseries"
[entityFilter]="currentEntityFilter"/>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.geofencing-zone-groups' | translate }}">
{{ 'calculated-fields.geofencing-zone-groups' | translate }}
</div>
<tb-calculated-field-geofencing-zone-groups-table formControlName="zoneGroups"
[entityId]="entityId"
[tenantId]="tenantId"
[entityName]="entityName"/>
<div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity">
<mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled">
<div tb-hint-tooltip-icon="{{'calculated-fields.hint.zone-group-refresh-interval' | translate}}">
{{ 'calculated-fields.zone-group-refresh-interval' | translate }}
</div>
</mat-slide-toggle>
<div class="flex flex-row items-center justify-start gap-2">
<tb-time-unit-input required
inlineField
requiredText="{{ 'calculated-fields.hint.zone-group-refresh-interval-required' | translate }}"
minErrorText="{{ 'calculated-fields.hint.zone-group-refresh-interval-min' | translate: {min: minAllowedScheduledUpdateIntervalInSecForCF} }}"
[minTime]="minAllowedScheduledUpdateIntervalInSecForCF"
formControlName="scheduledUpdateInterval">
</tb-time-unit-input>
</div>
</div>
</div>
<tb-calculate-field-output
formControlName="output"
[entityId]="entityId">
</tb-calculate-field-output>
</div>

157
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts

@ -0,0 +1,157 @@
///
/// 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, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
ArgumentEntityType,
CalculatedFieldGeofencing,
CalculatedFieldGeofencingConfiguration,
CalculatedFieldOutput,
CalculatedFieldType,
getCalculatedFieldCurrentEntityFilter,
OutputType
} from '@shared/models/calculated-field.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityFilter } from '@shared/models/query/query.models';
import { EntityId } from '@shared/models/id/entity-id';
@Component({
selector: 'tb-geofencing-configuration',
templateUrl: './geofencing-configuration.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GeofencingConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GeofencingConfigurationComponent),
multi: true
}
],
})
export class GeofencingConfigurationComponent implements ControlValueAccessor, Validator, OnInit {
@Input({required: true})
entityId: EntityId;
@Input({required: true})
tenantId: string;
@Input({required: true})
entityName: string;
readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF;
readonly DataKeyType = DataKeyType;
geofencingConfiguration = this.fb.group({
entityCoordinates: this.fb.group({
latitudeKeyName: [null, [Validators.required]],
longitudeKeyName: [null, [Validators.required]],
}),
zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}),
scheduledUpdateEnabled: [true],
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF],
output: this.fb.control<CalculatedFieldOutput>({scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries})
});
currentEntityFilter: EntityFilter;
isRelatedEntity: boolean;
private propagateChange: (config: CalculatedFieldGeofencingConfiguration) => void = () => { };
constructor(private fb: FormBuilder,
private store: Store<AppState>) {
this.geofencingConfiguration.get('zoneGroups').valueChanges
.pipe(takeUntilDestroyed())
.subscribe((zoneGroups: Record<string, CalculatedFieldGeofencing>) =>
this.checkRelatedEntity(zoneGroups)
);
this.geofencingConfiguration.get('scheduledUpdateEnabled').valueChanges
.pipe(takeUntilDestroyed())
.subscribe((value: boolean) =>
this.checkScheduledUpdateEnabled(value)
);
this.geofencingConfiguration.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => {
this.updatedModel(this.geofencingConfiguration.getRawValue() as any);
})
}
ngOnInit() {
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
}
validate(): ValidationErrors | null {
return this.geofencingConfiguration.valid ? null : { geofencingConfigError: false };
}
writeValue(config: CalculatedFieldGeofencingConfiguration): void {
this.geofencingConfiguration.patchValue(config, {emitEvent: false});
this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value);
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value);
}
registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void {
this.propagateChange = fn;
}
registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.geofencingConfiguration.disable({emitEvent: false});
} else {
this.geofencingConfiguration.enable({emitEvent: false});
this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value);
}
}
private updatedModel(value: CalculatedFieldGeofencingConfiguration) {
value.type = CalculatedFieldType.GEOFENCING;
this.propagateChange(value)
}
private checkScheduledUpdateEnabled(value: boolean) {
if (value) {
this.geofencingConfiguration.get('scheduledUpdateInterval').enable({emitEvent: false});
} else {
this.geofencingConfiguration.get('scheduledUpdateInterval').disable({emitEvent: false});
}
}
private checkRelatedEntity(zoneGroups: Record<string, CalculatedFieldGeofencing>) {
this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery);
}
}

52
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts

@ -0,0 +1,52 @@
///
/// 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 {
CalculatedFieldGeofencingZoneGroupsTableComponent
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component';
import {
CalculatedFieldGeofencingZoneGroupsPanelComponent
} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component';
import { SharedModule } from '@shared/shared.module';
import {
GeofencingConfigurationComponent
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component';
import {
CalculatedFieldOutputModule
} from '@home/components/calculated-fields/components/output/caclculate-field-output.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
CalculatedFieldOutputModule
],
declarations: [
CalculatedFieldGeofencingZoneGroupsTableComponent,
CalculatedFieldGeofencingZoneGroupsPanelComponent,
GeofencingConfigurationComponent
],
exports: [
CalculatedFieldGeofencingZoneGroupsTableComponent,
CalculatedFieldGeofencingZoneGroupsPanelComponent,
GeofencingConfigurationComponent
]
})
export class GeofencingConfigurationModule {
}

36
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/caclculate-field-output.module.ts

@ -0,0 +1,36 @@
///
/// 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 {
CalculatedFieldOutputComponent
} from '@home/components/calculated-fields/components/output/calculated-field-output.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
],
declarations: [
CalculatedFieldOutputComponent
],
exports: [
CalculatedFieldOutputComponent
]
})
export class CalculatedFieldOutputModule { }

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

@ -0,0 +1,86 @@
<!--
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 class="tb-form-panel" [formGroup]="outputForm">
<div class="tb-form-panel-title">{{ 'calculated-fields.output' | translate }}</div>
<div class="flex items-center gap-3">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label>
<mat-select formControlName="type">
@for (type of outputTypes; track type) {
<mat-option [value]="type">{{ OutputTypeTranslations.get(type) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (outputForm.get('type').value === OutputType.Attribute
&& (entityId.entityType === EntityType.DEVICE || entityId.entityType === EntityType.DEVICE_PROFILE)) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.attribute-scope' | translate }}</mat-label>
<mat-select formControlName="scope" class="w-full">
<mat-option [value]="AttributeScope.SERVER_SCOPE">
{{ 'calculated-fields.server-attributes' | translate }}
</mat-option>
<mat-option [value]="AttributeScope.SHARED_SCOPE">
{{ 'calculated-fields.shared-attributes' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
}
</div>
@if (simpleMode) {
<div class="flex items-start gap-3">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>
{{
(outputForm.get('type').value === OutputType.Timeseries
? 'calculated-fields.timeseries-key'
: 'calculated-fields.attribute-key')
| translate
}}
</mat-label>
<input matInput formControlName="name" required>
@if (outputForm.get('name').errors && outputForm.get('name').touched) {
<mat-error>
@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 }}
}
</mat-error>
}
</mat-form-field>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.decimals-by-default' | translate }}</mat-label>
<input matInput type="number" formControlName="decimalsByDefault">
@if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) {
<mat-error>{{ 'calculated-fields.hint.decimals-range' | translate }}</mat-error>
}
</mat-form-field>
</div>
<ng-content select=".simpleMode"></ng-content>
<!-- <div class="tb-form-row" [formGroup]="configFormGroup"-->
<!-- *ngIf="outputFormGroup.get('type').value === OutputType.Timeseries">-->
<!-- <mat-slide-toggle class="mat-slide" formControlName="useLatestTs">-->
<!-- <div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate>-->
<!-- calculated-fields.use-latest-timestamp-->
<!-- </div>-->
<!-- </mat-slide-toggle>-->
<!-- </div>-->
}
</div>

148
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -0,0 +1,148 @@
///
/// 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, DestroyRef, forwardRef, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import {
CalculatedFieldOutput,
CalculatedFieldSimpleOutput,
OutputType,
OutputTypeTranslations
} from '@shared/models/calculated-field.models';
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';
@Component({
selector: 'tb-calculate-field-output',
templateUrl: './calculated-field-output.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CalculatedFieldOutputComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CalculatedFieldOutputComponent),
multi: true
}
],
})
export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input()
simpleMode = false;
@Input({required: true})
entityId: EntityId;
readonly outputTypes = Object.values(OutputType) as OutputType[];
readonly OutputType = OutputType;
readonly AttributeScope = AttributeScope;
readonly OutputTypeTranslations = OutputTypeTranslations;
readonly EntityType = EntityType;
private fb = inject(FormBuilder);
private destroyRef = inject(DestroyRef);
outputForm = this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}],
type: [OutputType.Timeseries],
decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]],
});
private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { };
ngOnInit() {
this.outputForm.get('type').valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(type => this.toggleScopeByOutputType(type));
this.updatedFormWithMode();
this.outputForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => {
this.updatedModel(value)
})
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (propName === 'simpleMode') {
this.updatedFormWithMode();
if (!change.firstChange) {
this.outputForm.updateValueAndValidity();
}
}
}
}
}
validate(): ValidationErrors | null {
return this.outputForm.valid ? null : {outputConfig: false};
}
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void {
this.outputForm.patchValue(value, {emitEvent: false});
this.outputForm.get('type').updateValueAndValidity({onlySelf: true});
}
registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void {
this.propagateChange = fn;
}
registerOnTouched(_: any): void { }
private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) {
if (this.simpleMode && 'name' in value) {
value.name = value.name?.trim() ?? '';
}
this.propagateChange(value);
}
private toggleScopeByOutputType(type: OutputType): void {
if (type === OutputType.Attribute) {
this.outputForm.get('scope').enable({emitEvent: false});
} else {
this.outputForm.get('scope').disable({emitEvent: false});
}
}
private updatedFormWithMode(): void {
if (this.simpleMode) {
this.outputForm.get('name').enable({emitEvent: false});
this.outputForm.get('decimalsByDefault').enable({emitEvent: false});
} else {
this.outputForm.get('name').disable({emitEvent: false});
this.outputForm.get('decimalsByDefault').disable({emitEvent: false});
}
}
}

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts

@ -15,7 +15,5 @@
///
export * from './dialog/calculated-field-dialog.component';
export * from './arguments-table/calculated-field-arguments-table.component';
export * from './panel/calculated-field-argument-panel.component';
export * from './debug-dialog/calculated-field-debug-dialog.component';
export * from './test-dialog/calculated-field-script-test-dialog.component';

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component.html

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component.scss

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component.ts

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html → ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component.html

0
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component.scss

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts → ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component.ts

@ -43,7 +43,9 @@ import {
CalculatedFieldArgumentValue,
CalculatedFieldType,
} from '@shared/models/calculated-field.models';
import { CalculatedFieldArgumentPanelComponent } from '@home/components/calculated-fields/components/public-api';
import {
CalculatedFieldArgumentPanelComponent
} from '@home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

98
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html

@ -0,0 +1,98 @@
<!--
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]="simpleConfiguration" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="entityId"
[tenantId]="tenantId"
[entityName]="entityName"
[calculatedFieldType]="(isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE)" />
</div>
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">
{{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }}
</div>
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic" [class.hidden]="isScript">
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'(temperature - 32) / 1.8'"
required>
<div matSuffix
class="pr-2"
[tb-help-popup]="'math/math-methods_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
@if (simpleConfiguration.get('expressionSIMPLE').errors && simpleConfiguration.get('expressionSIMPLE').touched) {
<mat-error>
@if (simpleConfiguration.get('expressionSIMPLE').hasError('required')) {
{{ 'calculated-fields.hint.expression-required' | translate }}
} @else if (simpleConfiguration.get('expressionSIMPLE').hasError('pattern')) {
{{ 'calculated-fields.hint.expression-invalid' | translate }}
} @else if (simpleConfiguration.get('expressionSIMPLE').hasError('maxLength')) {
{{ 'calculated-fields.hint.expression-max-length' | translate }}
}
</mat-error>
} @else {
<mat-hint>{{ 'calculated-fields.hint.expression' | translate }}</mat-hint>
}
</mat-form-field>
<div [class.hidden]="!isScript">
<tb-js-func required
formControlName="expressionSCRIPT"
functionName="calculate"
[functionArgs]="functionArgs$ | async"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
[highlightRules]="argumentsHighlightRules$ | async"
[editorCompleter]="argumentsEditorCompleter$ | async"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="simpleConfiguration.get('arguments').invalid"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="simpleConfiguration.get('arguments').invalid">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</div>
</div>
<tb-calculate-field-output formControlName="output" [simpleMode]="!isScript" [entityId]="entityId">
<div class="tb-form-row simpleMode"
[class.!hidden]="simpleConfiguration.get('output').value.type !== OutputType.Timeseries">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate>
calculated-fields.use-latest-timestamp
</div>
</mat-slide-toggle>
</div>
</tb-calculate-field-output>
</div>

205
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.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, OnChanges, SimpleChanges } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import {
calculatedFieldDefaultScript,
CalculatedFieldScriptConfiguration,
CalculatedFieldSimpleConfiguration,
CalculatedFieldSimpleOutput,
CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
OutputType
} from '@shared/models/calculated-field.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { deepClone } from '@core/utils';
import { EntityId } from '@shared/models/id/entity-id';
import { Observable } from 'rxjs';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { map } from 'rxjs/operators';
type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration;
@Component({
selector: 'tb-simple-configuration',
templateUrl: './simple-configuration.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SimpleConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SimpleConfigurationComponent),
multi: true
}
],
})
export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges {
@Input()
isScript: boolean;
@Input()
entityId: EntityId;
@Input()
tenantId: string;
@Input()
entityName: string;
@Input()
testScript$: Observable<string>;
simpleConfiguration = this.fb.group({
arguments: this.fb.control({}),
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
expressionSCRIPT: [calculatedFieldDefaultScript],
output: this.fb.control<CalculatedFieldSimpleOutput>({
name: '',
scope: AttributeScope.SERVER_SCOPE,
type: OutputType.Timeseries,
decimalsByDefault: null
}),
useLatestTs: [false]
});
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType;
functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)])
);
argumentsEditorCompleter$ = this.simpleConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {}))
);
argumentsHighlightRules$ = this.simpleConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj))
);
private propagateChange: (config: SimpeConfiguration) => void = () => { };
constructor(private fb: FormBuilder) {
this.simpleConfiguration.get('output').valueChanges.pipe(
takeUntilDestroyed(),
).subscribe(() => {
this.toggleScopeByOutputType();
});
this.simpleConfiguration.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => {
const { expressionSIMPLE, expressionSCRIPT, ...config } = value;
const cfConfig = config as SimpeConfiguration;
cfConfig.expression = this.isScript ? expressionSCRIPT : expressionSIMPLE;
this.updatedModel(cfConfig);
})
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue) {
if (propName === 'isScript') {
this.updatedFormWithScript();
if (!change.firstChange) {
this.simpleConfiguration.updateValueAndValidity();
}
}
}
}
}
validate(): ValidationErrors | null {
return this.simpleConfiguration.valid ? null : {invalidSimpleConfig: false};
}
writeValue(value: SimpeConfiguration): void {
const formValue: any = deepClone(value);
if (this.isScript) {
formValue.expressionSCRIPT = formValue.expression;
} else {
formValue.expressionSIMPLE = formValue.expression;
}
this.simpleConfiguration.patchValue(formValue, {emitEvent: false});
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
this.updatedFormWithScript();
}
registerOnChange(fn: (config: SimpeConfiguration) => void): void {
this.propagateChange = fn;
}
registerOnTouched(_: any): void {
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.simpleConfiguration.disable({emitEvent: false});
} else {
this.simpleConfiguration.enable({emitEvent: false});
this.updatedFormWithScript();
}
}
onTestScript() {
this.testScript$?.subscribe((expression) => {
this.simpleConfiguration.get('expressionSCRIPT').setValue(expression);
this.simpleConfiguration.get('expressionSCRIPT').markAsDirty();
})
}
private updatedModel(value: SimpeConfiguration): void {
value.type = this.isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE;
this.propagateChange(value);
}
private updatedFormWithScript() {
if (this.isScript) {
this.simpleConfiguration.get('expressionSIMPLE').disable({emitEvent: false});
this.simpleConfiguration.get('expressionSCRIPT').enable({emitEvent: false});
} else {
this.simpleConfiguration.get('expressionSIMPLE').enable({emitEvent: false});
this.simpleConfiguration.get('expressionSCRIPT').disable({emitEvent: false});
}
this.toggleScopeByOutputType();
}
private toggleScopeByOutputType(): void {
if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) {
this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false});
} else {
this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false});
}
}
}

48
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts

@ -0,0 +1,48 @@
///
/// 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 {
SimpleConfigurationComponent
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component';
import {
CalculatedFieldArgumentPanelComponent
} from '@home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component';
import {
CalculatedFieldOutputModule
} from '@home/components/calculated-fields/components/output/caclculate-field-output.module';
import {
CalculatedFieldArgumentsTableComponent
} from '@home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
CalculatedFieldOutputModule,
],
declarations: [
SimpleConfigurationComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldArgumentsTableComponent
],
exports: [
SimpleConfigurationComponent
]
})
export class SimpleConfigurationModule {}

41
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -183,36 +183,13 @@ import {
} from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component';
import { EntityChipsComponent } from '@home/components/entity/entity-chips.component';
import { DashboardViewComponent } from '@home/components/dashboard-view/dashboard-view.component';
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
import { CalculatedFieldDialogComponent } from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component';
import {
EntityDebugSettingsButtonComponent
} from '@home/components/entity/debug/entity-debug-settings-button.component';
import {
CalculatedFieldArgumentsTableComponent
} from '@home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component';
import {
CalculatedFieldArgumentPanelComponent
} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component';
import {
CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component';
import {
CalculatedFieldScriptTestDialogComponent
} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component';
import {
CalculatedFieldTestArgumentsComponent
} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component';
import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component';
import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component';
import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component";
import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component";
import {
CalculatedFieldGeofencingZoneGroupsTableComponent
} from '@home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component';
import {
CalculatedFieldGeofencingZoneGroupsPanelComponent
} from '@home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component';
@NgModule({
declarations:
@ -357,15 +334,6 @@ import {
SendNotificationButtonComponent,
EntityChipsComponent,
DashboardViewComponent,
CalculatedFieldsTableComponent,
CalculatedFieldDialogComponent,
CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CalculatedFieldGeofencingZoneGroupsTableComponent,
CalculatedFieldGeofencingZoneGroupsPanelComponent,
CheckConnectivityDialogComponent,
AIModelDialogComponent,
ResourcesDialogComponent,
@ -508,15 +476,6 @@ import {
SendNotificationButtonComponent,
EntityChipsComponent,
DashboardViewComponent,
CalculatedFieldsTableComponent,
CalculatedFieldDialogComponent,
CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
CalculatedFieldGeofencingZoneGroupsTableComponent,
CalculatedFieldGeofencingZoneGroupsPanelComponent,
CheckConnectivityDialogComponent,
AIModelDialogComponent,
ResourcesDialogComponent,

4
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts

@ -20,6 +20,7 @@ import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { AssetProfileTabsComponent } from './asset-profile-tabs.component';
import { AssetProfileRoutingModule } from './asset-profile-routing.module';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
@NgModule({
declarations: [
@ -29,7 +30,8 @@ import { AssetProfileRoutingModule } from './asset-profile-routing.module';
CommonModule,
SharedModule,
HomeComponentsModule,
AssetProfileRoutingModule
CalculatedFieldsModule,
AssetProfileRoutingModule,
]
})
export class AssetProfileModule { }

4
ui-ngx/src/app/modules/home/pages/asset/asset.module.ts

@ -23,6 +23,7 @@ import { AssetTableHeaderComponent } from './asset-table-header.component';
import { AssetRoutingModule } from './asset-routing.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { AssetTabsComponent } from '@home/pages/asset/asset-tabs.component';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
@NgModule({
declarations: [
@ -35,7 +36,8 @@ import { AssetTabsComponent } from '@home/pages/asset/asset-tabs.component';
SharedModule,
HomeComponentsModule,
HomeDialogsModule,
AssetRoutingModule
CalculatedFieldsModule,
AssetRoutingModule,
]
})
export class AssetModule { }

2
ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts

@ -20,6 +20,7 @@ import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { DeviceProfileTabsComponent } from './device-profile-tabs.component';
import { DeviceProfileRoutingModule } from './device-profile-routing.module';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
@NgModule({
declarations: [
@ -29,6 +30,7 @@ import { DeviceProfileRoutingModule } from './device-profile-routing.module';
CommonModule,
SharedModule,
HomeComponentsModule,
CalculatedFieldsModule,
DeviceProfileRoutingModule
]
})

2
ui-ngx/src/app/modules/home/pages/device/device.module.ts

@ -36,6 +36,7 @@ import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-tr
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module';
import { DeviceCheckConnectivityDialogComponent } from './device-check-connectivity-dialog.component';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
@NgModule({
declarations: [
@ -61,6 +62,7 @@ import { DeviceCheckConnectivityDialogComponent } from './device-check-connectiv
HomeDialogsModule,
DeviceCredentialsModule,
DeviceProfileCommonModule,
CalculatedFieldsModule,
DeviceRoutingModule
]
})

2
ui-ngx/src/app/shared/components/time-unit-input.component.ts

@ -178,7 +178,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
this.timeInputForm.disable({emitEvent: false});
} else {
this.timeInputForm.enable({emitEvent: false});
if(this.timeInputForm.invalid) {
if(!this.timeInputForm.valid) {
setTimeout(() => this.updatedModel(this.timeInputForm.value, true))
}
}

55
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -31,16 +31,35 @@ import {
} from '@shared/models/ace/ace.models';
import { EntitySearchDirection } from '@shared/models/relation.models';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
configuration: CalculatedFieldConfiguration;
type: CalculatedFieldType;
interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
entityId: EntityId;
}
export interface CalculatedFieldSimple extends BaseCalculatedField {
type: CalculatedFieldType.SIMPLE;
configuration: CalculatedFieldSimpleConfiguration;
}
export interface CalculatedFieldScript extends BaseCalculatedField {
type: CalculatedFieldType.SCRIPT;
configuration: CalculatedFieldScriptConfiguration;
}
export interface CalculatedFieldGeofencing extends BaseCalculatedField {
type: CalculatedFieldType.GEOFENCING;
configuration: CalculatedFieldGeofencingConfiguration;
}
export type CalculatedField =
| CalculatedFieldSimple
| CalculatedFieldScript
| CalculatedFieldGeofencing;
export enum CalculatedFieldType {
SIMPLE = 'SIMPLE',
SCRIPT = 'SCRIPT',
GEOFENCING = 'GEOFENCING'
GEOFENCING = 'GEOFENCING',
PROPAGATION = 'PROPAGATION'
}
export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, string>(
@ -48,22 +67,44 @@ export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, stri
[CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'],
[CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'],
[CalculatedFieldType.GEOFENCING, 'calculated-fields.type.geofencing'],
[CalculatedFieldType.PROPAGATION, 'calculated-fields.type.propagation'],
]
)
export interface CalculatedFieldConfiguration {
type: CalculatedFieldType;
export type CalculatedFieldConfiguration =
| CalculatedFieldSimpleConfiguration
| CalculatedFieldScriptConfiguration
| CalculatedFieldGeofencingConfiguration;
export interface CalculatedFieldSimpleConfiguration {
type: CalculatedFieldType.SIMPLE;
expression?: string;
arguments?: Record<string, CalculatedFieldArgument>;
output: CalculatedFieldSimpleOutput;
}
export interface CalculatedFieldScriptConfiguration {
type: CalculatedFieldType.SCRIPT;
expression?: string;
arguments?: Record<string, CalculatedFieldArgument>;
output: CalculatedFieldOutput;
}
export interface CalculatedFieldGeofencingConfiguration {
type: CalculatedFieldType.GEOFENCING;
zoneGroups?: Record<string, CalculatedFieldGeofencing>;
scheduledUpdateEnabled?: boolean;
scheduledUpdateInterval?: number;
output: CalculatedFieldOutput;
}
export interface CalculatedFieldOutput {
type: OutputType;
name: string;
scope?: AttributeScope;
}
export interface CalculatedFieldSimpleOutput extends CalculatedFieldOutput {
name: string;
decimalsByDefault?: number;
}

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1053,7 +1053,8 @@
"type": {
"simple": "Simple",
"script": "Script",
"geofencing" : "Geofencing"
"geofencing" : "Geofencing",
"propagation": "Propagation"
},
"arguments": "Arguments",
"decimals-by-default": "Decimals by default",

Loading…
Cancel
Save