Browse Source

UI: Add configuration propagate calculate field

pull/14107/head
Vladyslav_Prykhodko 11 months ago
parent
commit
fca2166170
  1. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts
  2. 70
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  3. 134
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  4. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss
  5. 113
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  6. 15
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  7. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.scss
  8. 58
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  9. 45
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts
  10. 116
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  11. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  12. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  13. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  14. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts
  15. 0
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts
  16. 99
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html
  17. 174
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  18. 44
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts
  19. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  20. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  21. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts
  22. 51
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  23. 23
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -40,6 +40,9 @@ import {
import { import {
SimpleConfigurationModule SimpleConfigurationModule
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module'; } from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module';
import {
PropagationConfigurationModule
} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -55,7 +58,8 @@ import {
GeofencingConfigurationModule, GeofencingConfigurationModule,
EntityDebugSettingsButtonComponent, EntityDebugSettingsButtonComponent,
HomeComponentsModule, HomeComponentsModule,
SimpleConfigurationModule SimpleConfigurationModule,
PropagationConfigurationModule,
], ],
exports: [ exports: [
CalculatedFieldsTableComponent, CalculatedFieldsTableComponent,

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

@ -40,10 +40,12 @@ import {
ArgumentType, ArgumentType,
CalculatedField, CalculatedField,
CalculatedFieldEventArguments, CalculatedFieldEventArguments,
CalculatedFieldScriptConfiguration,
CalculatedFieldType, CalculatedFieldType,
CalculatedFieldTypeTranslations, CalculatedFieldTypeTranslations,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
PropagationWithExpression,
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { import {
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogComponent,
@ -122,7 +124,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.columns.push(new DateEntityTableColumn<CalculatedField>('createdTime', 'common.created-time', this.datePipe, '150px')); this.columns.push(new DateEntityTableColumn<CalculatedField>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%')); this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%'));
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '70px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type)))); this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '80px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type))));
this.columns.push(expressionColumn); this.columns.push(expressionColumn);
this.cellActionDescriptors.push( this.cellActionDescriptors.push(
@ -156,7 +158,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
} }
private getExpressionLabel(entity: CalculatedField): string { private getExpressionLabel(entity: CalculatedField): string {
if (entity.type === CalculatedFieldType.SCRIPT) { if (entity.type === CalculatedFieldType.SCRIPT ||
entity.type === CalculatedFieldType.PROPAGATION && entity.configuration.applyExpressionToResolvedArguments === true) {
return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')'; return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')';
} else if (entity.type === CalculatedFieldType.SIMPLE) { } else if (entity.type === CalculatedFieldType.SIMPLE) {
return entity.configuration.expression ?? ''; return entity.configuration.expression ?? '';
@ -288,35 +291,42 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
} }
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> { private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> {
if (calculatedField.type === CalculatedFieldType.GEOFENCING || calculatedField.type === CalculatedFieldType.SIMPLE) { if (
calculatedField.type === CalculatedFieldType.SCRIPT ||
(calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true)
) {
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)
? {...argumentsObj[key], type}
: type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()};
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression: (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)
}
}),
);
} else {
return of(null); 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)
? { ...argumentsObj[key], type }
: type === ArgumentType.Rolling ? { values: [], type } : { value: '', type, ts: new Date().getTime() };
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression: calculatedField.configuration.expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({ entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression } }, true)
}
}),
);
} }
} }

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

@ -19,62 +19,34 @@
<div class="tb-form-panel no-border no-padding mb-2"> <div class="tb-form-panel no-border no-padding mb-2">
<div class="tb-form-panel-title">{{ 'calculated-fields.argument-settings' | translate }}</div> <div class="tb-form-panel-title">{{ 'calculated-fields.argument-settings' | translate }}</div>
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">
<div class="tb-form-row"> @if (!isOutputKey) {
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-name' | translate }}</div> <ng-container *ngTemplateOutlet="argumentNameTemplate; context: {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> label: 'calculated-fields.argument-name',
<input matInput autocomplete="off" name="value" formControlName="argumentName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/> required: 'calculated-fields.hint.argument-name-required',
@if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { duplicate: 'calculated-fields.hint.argument-name-duplicate',
<mat-icon matSuffix pattern: 'calculated-fields.hint.argument-name-pattern',
matTooltipPosition="above" maxlength: 'calculated-fields.hint.argument-name-max-length',
matTooltipClass="tb-error-tooltip" forbidden: 'calculated-fields.hint.argument-name-forbidden'
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate" }"></ng-container>
class="tb-error"> }
warning <ng-container>
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-duplicate' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
<ng-container [formGroup]="refEntityIdFormGroup">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width">{{ 'entity.entity-type' | translate }}</div> <div class="fixed-title-width">{{ 'entity.entity-type' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType"> <mat-select [formControl]="argumentType">
@for (type of argumentEntityTypes; track type) { @for (type of argumentEntityTypes; track type) {
<mat-option [value]="type">{{ ArgumentEntityTypeTranslations.get(type) | translate }}</mat-option> <mat-option [value]="type">{{ ArgumentEntityTypeTranslations.get(type) | translate }}</mat-option>
} }
</mat-select> </mat-select>
@if (argumentType.touched && argumentType.hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.entity-type-required' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field> </mat-form-field>
</div> </div>
@if (ArgumentEntityTypeParamsMap.has(entityType)) { @if (ArgumentEntityTypeParamsMap.has(entityType)) {
@ -83,7 +55,8 @@
<tb-entity-autocomplete <tb-entity-autocomplete
class="flex flex-1" class="flex flex-1"
#entityAutocomplete #entityAutocomplete
formControlName="id" formControlName="refEntityId"
useFullEntityId
inlineField inlineField
[placeholder]="'action.set' | translate" [placeholder]="'action.set' | translate"
[required]="true" [required]="true"
@ -158,6 +131,16 @@
} }
} }
</ng-container> </ng-container>
@if (isOutputKey) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: {
label: 'calculated-fields.output-key',
required: 'calculated-fields.hint.output-key-required',
duplicate: 'calculated-fields.hint.output-key-duplicate',
pattern: 'calculated-fields.hint.output-key-pattern',
maxlength: 'calculated-fields.hint.output-key-max-length',
forbidden: 'calculated-fields.hint.output-key-forbidden'
}"></ng-container>
}
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.default-value' | translate }}</div> <div class="fixed-title-width">{{ 'calculated-fields.default-value' | translate }}</div>
@ -207,3 +190,54 @@
</button> </button>
</div> </div>
</div> </div>
<ng-template #argumentNameTemplate let-label="label" let-required="required" let-duplicate="duplicate"
let-pattern="pattern" let-maxlength="maxlength" let-forbidden="forbidden">
<div class="tb-form-row" [formGroup]="argumentFormGroup">
<div class="fixed-title-width tb-required">{{ label | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="argumentName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="required | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="duplicate | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="pattern | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="maxlength | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="forbidden | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
</ng-template>

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

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

@ -14,7 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; import {
AfterViewInit,
ChangeDetectorRef,
Component,
DestroyRef,
Input,
OnInit,
output,
ViewChild
} from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants';
@ -25,7 +34,6 @@ import {
ArgumentType, ArgumentType,
ArgumentTypeTranslations, ArgumentTypeTranslations,
CalculatedFieldArgumentValue, CalculatedFieldArgumentValue,
CalculatedFieldType,
getCalculatedFieldCurrentEntityFilter getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { debounceTime, delay, distinctUntilChanged, filter } from 'rxjs/operators'; import { debounceTime, delay, distinctUntilChanged, filter } from 'rxjs/operators';
@ -43,6 +51,7 @@ import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { TenantId } from '@shared/models/id/tenant-id';
@Component({ @Component({
selector: 'tb-calculated-field-argument-panel', selector: 'tb-calculated-field-argument-panel',
@ -56,22 +65,23 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() entityId: EntityId; @Input() entityId: EntityId;
@Input() tenantId: string; @Input() tenantId: string;
@Input() entityName: string; @Input() entityName: string;
@Input() calculatedFieldType: CalculatedFieldType; @Input() isScript: boolean;
@Input() usedArgumentNames: string[]; @Input() usedArgumentNames: string[];
@Input() isOutputKey = false;
@Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[];
@ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
argumentsDataApplied = output<CalculatedFieldArgumentValue>(); argumentsDataApplied = output<CalculatedFieldArgumentValue>();
argumentType = this.fb.control(ArgumentEntityType.Current, Validators.required);
readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg;
readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10);
argumentFormGroup = this.fb.group({ argumentFormGroup = this.fb.group({
argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
refEntityId: this.fb.group({ refEntityId: [null],
entityType: [ArgumentEntityType.Current],
id: ['']
}),
refEntityKey: this.fb.group({ refEntityKey: this.fb.group({
type: [ArgumentType.LatestTelemetry, [Validators.required]], type: [ArgumentType.LatestTelemetry, [Validators.required]],
key: ['', [Validators.pattern(oneSpaceInsideRegex)]], key: ['', [Validators.pattern(oneSpaceInsideRegex)]],
@ -86,7 +96,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
entityFilter: EntityFilter; entityFilter: EntityFilter;
entityNameSubject = new BehaviorSubject<string>(null); entityNameSubject = new BehaviorSubject<string>(null);
readonly argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[];
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
readonly ArgumentType = ArgumentType; readonly ArgumentType = ArgumentType;
readonly DataKeyType = DataKeyType; readonly DataKeyType = DataKeyType;
@ -103,20 +112,17 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
private fb: FormBuilder, private fb: FormBuilder,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>, private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>,
private store: Store<AppState> private store: Store<AppState>,
private destroyRef: DestroyRef
) { ) {
this.observeEntityFilterChanges(); this.observeEntityFilterChanges();
this.observeEntityTypeChanges(); this.observeArgumentTypeChanges();
this.observeEntityKeyChanges(); this.observeEntityKeyChanges();
this.observeUpdatePosition(); this.observeUpdatePosition();
} }
get entityType(): ArgumentEntityType { get entityType(): ArgumentEntityType {
return this.argumentFormGroup.get('refEntityId').get('entityType').value; return this.argumentType.value;
}
get refEntityIdFormGroup(): FormGroup {
return this.argumentFormGroup.get('refEntityId') as FormGroup;
} }
get refEntityKeyFormGroup(): FormGroup { get refEntityKeyFormGroup(): FormGroup {
@ -130,14 +136,18 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
} }
ngOnInit(): void { ngOnInit(): void {
this.updatedArgumentType();
this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
this.updateEntityFilter(this.argument.refEntityId?.entityType, true); this.updateEntityFilter(this.entityType, true);
this.updatedRefEntityIdState(this.entityType);
this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType(); this.setInitialEntityKeyType();
this.setInitialEntityType();
this.setWatchKeyChange();
this.argumentTypes = Object.values(ArgumentType) this.argumentTypes = Object.values(ArgumentType)
.filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); .filter(type => type !== ArgumentType.Rolling || this.isScript);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -147,12 +157,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
} }
saveArgument(): void { saveArgument(): void {
const { refEntityId, ...restConfig } = this.argumentFormGroup.value; const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue;
const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; if (this.entityType === ArgumentEntityType.Tenant) {
if (refEntityId.entityType === ArgumentEntityType.Tenant) { value.refEntityId = new TenantId(this.tenantId) as any;
refEntityId.id = this.tenantId;
} }
if (refEntityId.entityType !== ArgumentEntityType.Current && refEntityId.entityType !== ArgumentEntityType.Tenant) { if (this.entityType !== ArgumentEntityType.Current && this.entityType !== ArgumentEntityType.Tenant) {
value.entityName = this.entityNameSubject.value; value.entityName = this.entityNameSubject.value;
} }
if (value.defaultValue) { if (value.defaultValue) {
@ -166,6 +175,14 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.popover.hide(); this.popover.hide();
} }
private updatedArgumentType(): void {
let argumentType = ArgumentEntityType.Current;
if (this.argument.refEntityId?.entityType) {
argumentType = this.argument.refEntityId.entityType;
}
this.argumentType.setValue(argumentType, {emitEvent: false});
}
private toggleByEntityKeyType(type: ArgumentType): void { private toggleByEntityKeyType(type: ArgumentType): void {
const isAttribute = type === ArgumentType.Attribute; const isAttribute = type === ArgumentType.Attribute;
const isRolling = type === ArgumentType.Rolling; const isRolling = type === ArgumentType.Rolling;
@ -205,26 +222,21 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
private observeEntityFilterChanges(): void { private observeEntityFilterChanges(): void {
merge( merge(
this.refEntityIdFormGroup.get('entityType').valueChanges, this.argumentType.valueChanges,
this.refEntityKeyFormGroup.get('type').valueChanges, this.refEntityKeyFormGroup.get('type').valueChanges,
this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), this.argumentFormGroup.get('refEntityId').valueChanges.pipe(filter(Boolean)),
this.refEntityKeyFormGroup.get('scope').valueChanges, this.refEntityKeyFormGroup.get('scope').valueChanges,
) )
.pipe(debounceTime(50), takeUntilDestroyed()) .pipe(debounceTime(50), takeUntilDestroyed())
.subscribe(() => this.updateEntityFilter(this.entityType)); .subscribe(() => this.updateEntityFilter(this.entityType));
} }
private observeEntityTypeChanges(): void { private observeArgumentTypeChanges(): void {
this.refEntityIdFormGroup.get('entityType').valueChanges this.argumentType.valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed()) .pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe(type => { .subscribe(type => {
this.argumentFormGroup.get('refEntityId').get('id').setValue(''); this.argumentFormGroup.get('refEntityId').setValue(null);
const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; this.updatedRefEntityIdState(type);
this.argumentFormGroup.get('refEntityId')
.get('id')[isEntityWithId ? 'enable' : 'disable']();
if (!isEntityWithId) {
this.entityNameSubject.next(null);
}
if (!this.enableAttributeScopeSelection) { if (!this.enableAttributeScopeSelection) {
this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE);
} }
@ -247,29 +259,56 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
} }
private setInitialEntityKeyType(): void { private setInitialEntityKeyType(): void {
if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argument.refEntityKey?.type === ArgumentType.Rolling) { if (!this.isScript && this.argument.refEntityKey?.type === ArgumentType.Rolling) {
const typeControl = this.argumentFormGroup.get('refEntityKey').get('type'); const typeControl = this.argumentFormGroup.get('refEntityKey').get('type');
typeControl.setValue(null); typeControl.setValue(null);
typeControl.markAsTouched(); typeControl.markAsTouched();
} }
} }
private setInitialEntityType() {
if (!this.argumentEntityTypes.includes(this.entityType)) {
this.argumentType.setValue(null);
this.argumentType.markAsTouched();
}
}
private setWatchKeyChange(): void {
if (this.isOutputKey) {
this.refEntityKeyFormGroup.get('key').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((key) => {
if (this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').setValue(key);
}
});
}
}
private forbiddenArgumentNameValidator(): ValidatorFn { private forbiddenArgumentNameValidator(): ValidatorFn {
return (control: FormControl) => { return (control: FormControl) => {
const trimmedValue = control.value.trim().toLowerCase(); const trimmedValue = control.value.trim().toLowerCase();
const forbiddenArgumentNames = ['ctx', 'e', 'pi']; const forbiddenArgumentNames = ['ctx', 'e', 'pi', 'propagationCtx'];
return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null;
}; };
} }
private observeUpdatePosition(): void { private observeUpdatePosition(): void {
merge( merge(
this.refEntityIdFormGroup.get('entityType').valueChanges, this.argumentType.valueChanges,
this.refEntityKeyFormGroup.get('type').valueChanges, this.refEntityKeyFormGroup.get('type').valueChanges,
this.argumentFormGroup.get('timeWindow').valueChanges, this.argumentFormGroup.get('timeWindow').valueChanges,
this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), this.argumentFormGroup.get('refEntityId').valueChanges.pipe(filter(Boolean)),
) )
.pipe(delay(50), takeUntilDestroyed()) .pipe(delay(50), takeUntilDestroyed())
.subscribe(() => this.popover.updatePosition()); .subscribe(() => this.popover.updatePosition());
} }
private updatedRefEntityIdState(type: ArgumentEntityType): void {
const isEntityWithId = !!type && type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current;
this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']();
if (!isEntityWithId) {
this.entityNameSubject.next(null);
}
}
} }

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

@ -21,7 +21,7 @@
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear> [matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'"> <ng-container [matColumnDef]="'name'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="!w-1/3 xs:!w-1/2"> <mat-header-cell mat-sort-header *matHeaderCellDef class="!w-1/3 xs:!w-1/2">
<div tbTruncateWithTooltip>{{ 'common.name' | translate }}</div> <div tbTruncateWithTooltip>{{ argumentNameColumn | translate }}</div>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let argument" class="argument-name-cell w-1/3 xs:w-1/2"> <mat-cell *matCellDef="let argument" class="argument-name-cell w-1/3 xs:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
@ -29,7 +29,7 @@
<tb-copy-button <tb-copy-button
class="copy-argument-name" class="copy-argument-name"
[copyText]="argument.argumentName" [copyText]="argument.argumentName"
tooltipText="{{ 'calculated-fields.copy-argument-name' | translate }}" tooltipText="{{ argumentNameColumnCopy | translate }}"
tooltipPosition="above" tooltipPosition="above"
icon="content_copy" icon="content_copy"
/> />
@ -37,7 +37,7 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="'entityType'"> <ng-container [matColumnDef]="'entityType'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="entity-type-header w-1/5 xs:hidden"> <mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/5 xs:hidden">
{{ 'entity.entity-type' | translate }} {{ 'entity.entity-type' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let argument" class="w-1/5 xs:hidden"> <mat-cell *matCellDef="let argument" class="w-1/5 xs:hidden">
@ -96,8 +96,7 @@
[matTooltip]="'action.edit' | translate" [matTooltip]="'action.edit' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon <mat-icon
[matBadgeHidden]="!(argument.refEntityKey.type === ArgumentType.Rolling [matBadgeHidden]="isEditButtonShowBadge(argument)"
&& calculatedFieldType === CalculatedFieldType.SIMPLE) && argument.refEntityId?.id !== NULL_UUID"
matBadgeColor="warn" matBadgeColor="warn"
matBadgeSize="small" matBadgeSize="small"
matBadge="*" matBadge="*"
@ -115,10 +114,8 @@
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row class="mat-row-select" <mat-header-row class="mat-row-select" *matHeaderRowDef="displayColumns"></mat-header-row>
*matHeaderRowDef="['name', 'entityType', 'target', 'type', 'key', 'actions']"></mat-header-row> <mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
<mat-row
*matRowDef="let argument; columns: ['name', 'entityType', 'target', 'type', 'key', 'actions']"></mat-row>
</table> </table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false" <div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center"> class="tb-prompt flex flex-1 items-end justify-center">

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

@ -62,7 +62,7 @@
} }
.arguments-table { .arguments-table {
.mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header { .mat-mdc-header-row.mat-row-select .mat-mdc-header-cell:nth-child(2) {
padding: 0 28px 0 0; padding: 0 28px 0 0;
} }
} }

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

@ -45,17 +45,17 @@ import {
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { import {
CalculatedFieldArgumentPanelComponent CalculatedFieldArgumentPanelComponent
} from '@home/components/calculated-fields/components/simple-configuration/calculated-field-argument-panel.component'; } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL, isEqual } from '@core/utils'; import { getEntityDetailsPageURL, isDefined, isEqual } from '@core/utils';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { MatSort } from '@angular/material/sort'; import { MatSort, SortDirection } from '@angular/material/sort';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -85,16 +85,22 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() entityId: EntityId; @Input() entityId: EntityId;
@Input() tenantId: string; @Input() tenantId: string;
@Input() entityName: string; @Input() entityName: string;
@Input() calculatedFieldType: CalculatedFieldType; @Input() isScript: boolean;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
errorText = ''; errorText = '';
argumentsFormArray = this.fb.array<CalculatedFieldArgumentValue>([]); argumentsFormArray = this.fb.array<CalculatedFieldArgumentValue>([]);
entityNameMap = new Map<string, string>(); entityNameMap = new Map<string, string>();
sortOrder = { direction: 'asc', property: '' }; sortOrder: { direction: SortDirection; property: string } = {direction: 'asc', property: ''};
dataSource = new CalculatedFieldArgumentDatasource(); dataSource = new CalculatedFieldArgumentDatasource();
argumentNameColumn = 'common.name';
argumentNameColumnCopy = 'calculated-fields.copy-argument-name';
displayColumns = ['name', 'entityType', 'target', 'type', 'key', 'actions'];
protected panelAdditionalCtx: Record<string, any>
readonly entityTypeTranslations = entityTypeTranslations; readonly entityTypeTranslations = entityTypeTranslations;
readonly ArgumentTypeTranslations = ArgumentTypeTranslations; readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
readonly ArgumentEntityType = ArgumentEntityType; readonly ArgumentEntityType = ArgumentEntityType;
@ -107,14 +113,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {}; private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {};
constructor( constructor(
private fb: FormBuilder, protected fb: FormBuilder,
private popoverService: TbPopoverService, protected popoverService: TbPopoverService,
private viewContainerRef: ViewContainerRef, protected viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
private renderer: Renderer2, protected renderer: Renderer2,
private entityService: EntityService, protected entityService: EntityService,
private destroyRef: DestroyRef, protected destroyRef: DestroyRef,
private store: Store<AppState> protected store: Store<AppState>
) { ) {
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => {
this.updateDataSource(value); this.updateDataSource(value);
@ -123,9 +129,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.calculatedFieldType?.previousValue if (isDefined(changes.isScript?.previousValue) && changes.isScript.currentValue !== changes.isScript.previousValue) {
&& changes.calculatedFieldType.currentValue !== changes.calculatedFieldType.previousValue) { this.changeIsScriptMode();
this.argumentsFormArray.updateValueAndValidity();
} }
} }
@ -141,7 +146,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(_): void {} registerOnTouched(_: any): void {}
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
this.updateErrorText(); this.updateErrorText();
@ -170,7 +175,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
index, index,
argument, argument,
entityId: this.entityId, entityId: this.entityId,
calculatedFieldType: this.calculatedFieldType, isScript: this.isScript,
buttonTitle: isExists ? 'action.apply' : 'action.add', buttonTitle: isExists ? 'action.apply' : 'action.add',
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName, entityName: this.entityName,
@ -181,8 +186,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
renderer: this.renderer, renderer: this.renderer,
componentType: CalculatedFieldArgumentPanelComponent, componentType: CalculatedFieldArgumentPanelComponent,
hostView: this.viewContainerRef, hostView: this.viewContainerRef,
preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'],
context: ctx, context: Object.assign(ctx, this.panelAdditionalCtx),
isModal: true isModal: true
}); });
this.popoverComponent.tbComponentRef.instance.argumentsDataApplied.subscribe(({ entityName, ...value }) => { this.popoverComponent.tbComponentRef.instance.argumentsDataApplied.subscribe(({ entityName, ...value }) => {
@ -205,9 +210,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
this.dataSource.loadData(sortedValue); this.dataSource.loadData(sortedValue);
} }
private updateErrorText(): void { protected updateErrorText(): void {
if (this.calculatedFieldType === CalculatedFieldType.SIMPLE if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) {
&& this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) {
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
} else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
@ -236,6 +240,14 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
return getEntityDetailsPageURL(id, type); return getEntityDetailsPageURL(id, type);
} }
protected changeIsScriptMode(): void {
this.argumentsFormArray.updateValueAndValidity();
}
protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean {
return !(argument.refEntityKey.type === ArgumentType.Rolling && !this.isScript) && argument.refEntityId?.id !== NULL_UUID
}
private populateArgumentsFormArray(argumentsObj: Record<string, CalculatedFieldArgument>): void { private populateArgumentsFormArray(argumentsObj: Record<string, CalculatedFieldArgument>): void {
Object.keys(argumentsObj).forEach(key => { Object.keys(argumentsObj).forEach(key => {
const value: CalculatedFieldArgumentValue = { const value: CalculatedFieldArgumentValue = {

45
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module.ts

@ -0,0 +1,45 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import {
CalculatedFieldArgumentPanelComponent
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component';
import {
CalculatedFieldArgumentsTableComponent
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component';
import {
PropagateArgumentsTableComponent
} from '@home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
],
declarations: [
CalculatedFieldArgumentPanelComponent,
CalculatedFieldArgumentsTableComponent,
PropagateArgumentsTableComponent
],
exports: [
CalculatedFieldArgumentsTableComponent,
PropagateArgumentsTableComponent
]
})
export class CalculatedFieldArgumentsTableModule {}

116
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts

@ -0,0 +1,116 @@
///
/// 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,
OnInit,
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, ArgumentType, CalculatedFieldArgumentValue } from '@shared/models/calculated-field.models';
import { isDefined } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({
selector: 'tb-propagate-arguments-table',
templateUrl: './calculated-field-arguments-table.component.html',
styleUrls: [`calculated-field-arguments-table.component.scss`],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PropagateArgumentsTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => PropagateArgumentsTableComponent),
multi: true
}
],
})
export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent implements OnInit {
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)
}
ngOnInit() {
this.updatedValue();
}
protected changeIsScriptMode(): void {
this.updatedValue();
super.changeIsScriptMode();
}
private updatedValue() {
if (this.isScript) {
this.argumentNameColumn = 'common.name';
this.argumentNameColumnCopy = 'calculated-fields.copy-argument-name';
this.displayColumns = ['name', 'entityType', 'target', 'type', 'key', 'actions'];
this.panelAdditionalCtx = null;
} else {
this.argumentNameColumn = 'calculated-fields.output-key';
this.argumentNameColumnCopy = 'calculated-fields.copy-output-key';
this.displayColumns = ['name', 'type', 'key', 'actions'];
this.panelAdditionalCtx = {
argumentEntityTypes: [ArgumentEntityType.Current],
isOutputKey: true
};
}
}
protected isEditButtonShowBadge(argument: CalculatedFieldArgumentValue): boolean {
if (!this.isScript && isDefined(argument?.refEntityId)) {
return false;
}
return super.isEditButtonShowBadge(argument);
}
protected updateErrorText(): void {
if (!this.isScript && this.argumentsFormArray.controls.some(control => isDefined(control.value?.refEntityId))) {
this.errorText = 'calculated-fields.hint.arguments-propagate-argument-entity-type';
} else if (!this.isScript && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) {
this.errorText = 'calculated-fields.hint.arguments-propagate-arguments-with-rolling';
} else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
} else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty';
} else {
this.errorText = '';
}
}
}

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

@ -67,13 +67,21 @@
<tb-geofencing-configuration formControlName="configuration" [entityId]="data.entityId" [entityName]="data.entityName" [tenantId]="data.tenantId"> <tb-geofencing-configuration formControlName="configuration" [entityId]="data.entityId" [entityName]="data.entityName" [tenantId]="data.tenantId">
</tb-geofencing-configuration> </tb-geofencing-configuration>
} }
@case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)">
</tb-propagation-configuration>
}
@default { @default {
<tb-simple-configuration formControlName="configuration" <tb-simple-configuration formControlName="configuration"
[entityId]="data.entityId" [entityId]="data.entityId"
[entityName]="data.entityName" [entityName]="data.entityName"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT" [isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT"
[testScript$]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
> >
</tb-simple-configuration> </tb-simple-configuration>
} }

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

@ -83,6 +83,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.observeIsLoading(); this.observeIsLoading();
this.observeType();
this.applyDialogData(); this.applyDialogData();
} }
@ -134,4 +135,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
} }
}); });
} }
private observeType(): void {
this.fieldFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((type) => {
if (type !== CalculatedFieldType.SIMPLE && type !== CalculatedFieldType.SCRIPT) {
this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false});
}
});
}
} }

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

@ -114,7 +114,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.geofencingConfiguration.valid ? null : { geofencingConfigError: false }; return this.geofencingConfiguration.valid || this.geofencingConfiguration.status === "DISABLED" ? null : { geofencingConfigError: false };
} }
writeValue(config: CalculatedFieldGeofencingConfiguration): void { writeValue(config: CalculatedFieldGeofencingConfiguration): void {

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

@ -28,7 +28,7 @@ import {
} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component'; } from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component';
import { import {
CalculatedFieldOutputModule CalculatedFieldOutputModule
} from '@home/components/calculated-fields/components/output/caclculate-field-output.module'; } from '@home/components/calculated-fields/components/output/calculated-field-output.module';
@NgModule({ @NgModule({
imports: [ imports: [

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

99
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html

@ -0,0 +1,99 @@
<!--
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]="propagateConfiguration" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.propagation-path-related-entities' | translate }}">
{{ 'calculated-fields.propagation-path-related-entities' | translate }}
</div>
<div class="flex gap-3 xs:flex-col">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label>{{ 'calculated-fields.direction' | translate }}</mat-label>
<mat-select formControlName="direction">
@for (direction of Directions; track direction) {
<mat-option [value]="direction">{{ PropagationDirectionTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
class="flex-1"
panelWidth=""
additionalClass=""
required
[label]="'calculated-fields.relation-type' | translate"
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
</div>
<div class="tb-form-panel">
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.data-propagate' | translate }}">
{{ 'calculated-fields.data-propagate' | translate }}
</div>
<tb-toggle-select formControlName="applyExpressionToResolvedArguments">
<tb-toggle-option [value]="false">{{ 'calculated-fields.propagate-type.arguments-only' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'calculated-fields.propagate-type.expression-result' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-propagate-arguments-table formControlName="arguments"
[entityId]="entityId"
[tenantId]="tenantId"
[entityName]="entityName"
[isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/>
</div>
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value">
<div class="tb-form-panel-title tb-required">
{{ 'calculated-fields.expression' | translate }}
</div>
<div>
<tb-js-func required
formControlName="expression"
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-expression-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="propagateConfiguration.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]="propagateConfiguration.get('arguments').invalid">
{{ 'calculated-fields.test-expression-function' | translate }}
</button>
</div>
</div>
</div>
<tb-calculate-field-output formControlName="output" [entityId]="entityId">
</tb-calculate-field-output>
</div>

174
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts

@ -0,0 +1,174 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { Observable, of } from 'rxjs';
import {
calculatedFieldDefaultScript,
CalculatedFieldOutput,
CalculatedFieldPropagationConfiguration,
CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
OutputType,
PropagationDirectionTranslations,
PropagationWithExpression
} from '@shared/models/calculated-field.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@app/shared/models/rule-node.models';
import { EntitySearchDirection } from '@shared/models/relation.models';
@Component({
selector: 'tb-propagation-configuration',
templateUrl: './propagation-configuration.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PropagationConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => PropagationConfigurationComponent),
multi: true
}
],
})
export class PropagationConfigurationComponent implements ControlValueAccessor, Validator {
@Input({required: true})
entityId: EntityId;
@Input({required: true})
tenantId: string;
@Input({required: true})
entityName: string;
@Input({required: true})
testScript: () => Observable<string>;
propagateConfiguration = this.fb.group({
arguments: this.fb.control({}),
applyExpressionToResolvedArguments: [false],
direction: [EntitySearchDirection.TO, Validators.required],
relationType: ['Contains', Validators.required],
expression: [calculatedFieldDefaultScript],
output: this.fb.control<CalculatedFieldOutput>({
scope: AttributeScope.SERVER_SCOPE,
type: OutputType.Timeseries,
}),
});
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType;
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>;
readonly PropagationDirectionTranslations = PropagationDirectionTranslations;
functionArgs$ = this.propagateConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)])
);
argumentsEditorCompleter$ = this.propagateConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {}))
);
argumentsHighlightRules$ = this.propagateConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj))
);
private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { };
constructor(private fb: FormBuilder) {
this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => {
this.updatedFormWithScript();
})
this.propagateConfiguration.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value: CalculatedFieldPropagationConfiguration) => {
this.updatedModel(value);
})
}
validate(): ValidationErrors | null {
return this.propagateConfiguration.valid || this.propagateConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false};
}
writeValue(value: PropagationWithExpression): void {
value.expression = value.expression ?? calculatedFieldDefaultScript;
this.propagateConfiguration.patchValue(value, {emitEvent: false});
this.updatedFormWithScript();
setTimeout(() => {
this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
});
}
registerOnChange(fn: (config: CalculatedFieldPropagationConfiguration) => void): void {
this.propagateChange = fn;
}
registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.propagateConfiguration.disable({emitEvent: false});
} else {
this.propagateConfiguration.enable({emitEvent: false});
this.updatedFormWithScript();
}
}
onTestScript() {
this.testScript().subscribe((expression) => {
this.propagateConfiguration.get('expression').setValue(expression);
this.propagateConfiguration.get('expression').markAsDirty();
})
}
fetchOptions(searchText: string): Observable<Array<string>> {
const search = searchText ? searchText?.toLowerCase() : '';
return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search))));
}
private updatedModel(value: CalculatedFieldPropagationConfiguration): void {
value.type = CalculatedFieldType.PROPAGATION;
this.propagateChange(value);
}
private updatedFormWithScript() {
if (this.propagateConfiguration.get('applyExpressionToResolvedArguments').value) {
this.propagateConfiguration.get('expression').enable({emitEvent: false});
} else {
this.propagateConfiguration.get('expression').disable({emitEvent: false});
}
}
}

44
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts

@ -0,0 +1,44 @@
///
/// 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 {
PropagationConfigurationComponent
} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
CalculatedFieldOutputModule,
CalculatedFieldArgumentsTableModule,
],
declarations: [
PropagationConfigurationComponent,
],
exports: [
PropagationConfigurationComponent,
]
})
export class PropagationConfigurationModule { }

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

@ -22,7 +22,7 @@
[entityId]="entityId" [entityId]="entityId"
[tenantId]="tenantId" [tenantId]="tenantId"
[entityName]="entityName" [entityName]="entityName"
[calculatedFieldType]="(isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE)" /> [isScript]="isScript" />
</div> </div>
<div class="tb-form-panel no-gap"> <div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required"> <div class="tb-form-panel-title tb-required">

21
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -66,17 +66,17 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
@Input() @Input()
isScript: boolean; isScript: boolean;
@Input() @Input({required: true})
entityId: EntityId; entityId: EntityId;
@Input() @Input({required: true})
tenantId: string; tenantId: string;
@Input() @Input({required: true})
entityName: string; entityName: string;
@Input() @Input({required: true})
testScript$: Observable<string>; testScript: () => Observable<string>;
simpleConfiguration = this.fb.group({ simpleConfiguration = this.fb.group({
arguments: this.fb.control({}), arguments: this.fb.control({}),
@ -92,7 +92,6 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
}); });
readonly ScriptLanguage = ScriptLanguage; readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType; readonly OutputType = OutputType;
functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe(
@ -141,19 +140,21 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.simpleConfiguration.valid ? null : {invalidSimpleConfig: false}; return this.simpleConfiguration.valid || this.simpleConfiguration.status === "DISABLED" ? null : {invalidSimpleConfig: false};
} }
writeValue(value: SimpeConfiguration): void { writeValue(value: SimpeConfiguration): void {
const formValue: any = deepClone(value); const formValue: any = deepClone(value);
if (this.isScript) { if (this.isScript) {
formValue.expressionSCRIPT = formValue.expression; formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript;
} else { } else {
formValue.expressionSIMPLE = formValue.expression; formValue.expressionSIMPLE = formValue.expression;
} }
this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); this.simpleConfiguration.patchValue(formValue, {emitEvent: false});
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
this.updatedFormWithScript(); this.updatedFormWithScript();
setTimeout(() => {
this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true});
});
} }
registerOnChange(fn: (config: SimpeConfiguration) => void): void { registerOnChange(fn: (config: SimpeConfiguration) => void): void {
@ -173,7 +174,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
} }
onTestScript() { onTestScript() {
this.testScript$?.subscribe((expression) => { this.testScript().subscribe((expression) => {
this.simpleConfiguration.get('expressionSCRIPT').setValue(expression); this.simpleConfiguration.get('expressionSCRIPT').setValue(expression);
this.simpleConfiguration.get('expressionSCRIPT').markAsDirty(); this.simpleConfiguration.get('expressionSCRIPT').markAsDirty();
}) })

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

@ -20,26 +20,22 @@ import { SharedModule } from '@shared/shared.module';
import { import {
SimpleConfigurationComponent SimpleConfigurationComponent
} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component'; } 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 { import {
CalculatedFieldOutputModule CalculatedFieldOutputModule
} from '@home/components/calculated-fields/components/output/caclculate-field-output.module'; } from '@home/components/calculated-fields/components/output/calculated-field-output.module';
import { import {
CalculatedFieldArgumentsTableComponent CalculatedFieldArgumentsTableModule
} from '@home/components/calculated-fields/components/simple-configuration/calculated-field-arguments-table.component'; } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
CalculatedFieldOutputModule, CalculatedFieldOutputModule,
CalculatedFieldArgumentsTableModule,
], ],
declarations: [ declarations: [
SimpleConfigurationComponent, SimpleConfigurationComponent,
CalculatedFieldArgumentPanelComponent,
CalculatedFieldArgumentsTableComponent
], ],
exports: [ exports: [
SimpleConfigurationComponent SimpleConfigurationComponent

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

@ -50,10 +50,16 @@ export interface CalculatedFieldGeofencing extends BaseCalculatedField {
configuration: CalculatedFieldGeofencingConfiguration; configuration: CalculatedFieldGeofencingConfiguration;
} }
export interface CalculatedFieldPropagation extends BaseCalculatedField {
type: CalculatedFieldType.PROPAGATION;
configuration: CalculatedFieldPropagationConfiguration;
}
export type CalculatedField = export type CalculatedField =
| CalculatedFieldSimple | CalculatedFieldSimple
| CalculatedFieldScript | CalculatedFieldScript
| CalculatedFieldGeofencing; | CalculatedFieldGeofencing
| CalculatedFieldPropagation;
export enum CalculatedFieldType { export enum CalculatedFieldType {
SIMPLE = 'SIMPLE', SIMPLE = 'SIMPLE',
@ -74,30 +80,52 @@ export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, stri
export type CalculatedFieldConfiguration = export type CalculatedFieldConfiguration =
| CalculatedFieldSimpleConfiguration | CalculatedFieldSimpleConfiguration
| CalculatedFieldScriptConfiguration | CalculatedFieldScriptConfiguration
| CalculatedFieldGeofencingConfiguration; | CalculatedFieldGeofencingConfiguration
| CalculatedFieldPropagationConfiguration;
export interface CalculatedFieldSimpleConfiguration { export interface CalculatedFieldSimpleConfiguration {
type: CalculatedFieldType.SIMPLE; type: CalculatedFieldType.SIMPLE;
expression?: string; expression: string;
arguments?: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
output: CalculatedFieldSimpleOutput; output: CalculatedFieldSimpleOutput;
} }
export interface CalculatedFieldScriptConfiguration { export interface CalculatedFieldScriptConfiguration {
type: CalculatedFieldType.SCRIPT; type: CalculatedFieldType.SCRIPT;
expression?: string; expression: string;
arguments?: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
output: CalculatedFieldOutput; output: CalculatedFieldOutput;
} }
export interface CalculatedFieldGeofencingConfiguration { export interface CalculatedFieldGeofencingConfiguration {
type: CalculatedFieldType.GEOFENCING; type: CalculatedFieldType.GEOFENCING;
zoneGroups?: Record<string, CalculatedFieldGeofencing>; zoneGroups: Record<string, CalculatedFieldGeofencing>;
scheduledUpdateEnabled?: boolean; scheduledUpdateEnabled: boolean;
scheduledUpdateInterval?: number; scheduledUpdateInterval?: number;
output: CalculatedFieldOutput; output: CalculatedFieldOutput;
} }
interface BasePropagationConfiguration {
type: CalculatedFieldType.PROPAGATION;
direction: EntitySearchDirection;
relationType: string;
arguments: Record<string, CalculatedFieldArgument>;
output: CalculatedFieldOutput;
}
export interface PropagationWithNoExpression extends BasePropagationConfiguration {
applyExpressionToResolvedArguments: false;
}
export interface PropagationWithExpression extends BasePropagationConfiguration {
applyExpressionToResolvedArguments: true;
expression: string;
}
export type CalculatedFieldPropagationConfiguration =
| PropagationWithNoExpression
| PropagationWithExpression;
export interface CalculatedFieldOutput { export interface CalculatedFieldOutput {
type: OutputType; type: OutputType;
scope?: AttributeScope; scope?: AttributeScope;
@ -156,6 +184,13 @@ export const GeofencingDirectionLevelTranslations = new Map<EntitySearchDirectio
] ]
) )
export const PropagationDirectionTranslations = new Map<EntitySearchDirection, string>(
[
[EntitySearchDirection.FROM, 'calculated-fields.direction-down-child'],
[EntitySearchDirection.TO, 'calculated-fields.direction-up-parent'],
]
)
export enum ArgumentType { export enum ArgumentType {
Attribute = 'ATTRIBUTE', Attribute = 'ATTRIBUTE',
LatestTelemetry = 'TS_LATEST', LatestTelemetry = 'TS_LATEST',

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

@ -1064,6 +1064,7 @@
"datasource": "Datasource", "datasource": "Datasource",
"add-argument": "Add argument", "add-argument": "Add argument",
"test-script-function": "Test script function", "test-script-function": "Test script function",
"test-expression-function": "Test expression function",
"no-arguments": "No arguments configured", "no-arguments": "No arguments configured",
"argument-settings": "Argument settings", "argument-settings": "Argument settings",
"argument-current": "Current entity", "argument-current": "Current entity",
@ -1139,14 +1140,26 @@
"level": "Level", "level": "Level",
"direction-level": "Direction", "direction-level": "Direction",
"direction-up": "Up", "direction-up": "Up",
"direction-up-parent": "Up to parent",
"direction-down": "Down", "direction-down": "Down",
"direction-down-child": "Down to child",
"add-level": "Add level", "add-level": "Add level",
"delete-level": "Delete level", "delete-level": "Delete level",
"no-level": "No level configured", "no-level": "No level configured",
"levels-required": "At least one level must be configured.", "levels-required": "At least one level must be configured.",
"max-allowed-levels-error": "Relation level exceeds the maximum allowed.", "max-allowed-levels-error": "Relation level exceeds the maximum allowed.",
"propagation-path-related-entities": "Propagation path to related entities",
"propagate-type": {
"arguments-only": "Arguments only",
"expression-result": "Expression result"
},
"data-propagate": "Data to propagate",
"output-key": "Output key",
"copy-output-key": "Copy output key",
"hint": { "hint": {
"arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "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.",
"arguments-propagate-argument-entity-type": "Entity type is incompatible with 'Arguments only' propagation.",
"arguments-empty": "Arguments should not be empty.", "arguments-empty": "Arguments should not be empty.",
"expression-required": "Expression is required.", "expression-required": "Expression is required.",
"expression-invalid": "Expression is invalid", "expression-invalid": "Expression is invalid",
@ -1156,6 +1169,12 @@
"argument-name-duplicate": "Argument with such name already exists.", "argument-name-duplicate": "Argument with such name already exists.",
"argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-max-length": "Argument name should be less than 256 characters.",
"argument-name-forbidden": "Argument name is reserved and cannot be used.", "argument-name-forbidden": "Argument name is reserved and cannot be used.",
"output-key-required": "Output key is required.",
"output-key-pattern": "Output key is invalid.",
"output-key-duplicate": "Key with such name already exists.",
"output-key-max-length": "Output key should be less than 256 characters.",
"output-key-forbidden": "Output key is reserved and cannot be used.",
"entity-type-required": "Entity type is required",
"name-required": "Mame is required.", "name-required": "Mame is required.",
"name-pattern": "Name is invalid.", "name-pattern": "Name is invalid.",
"name-duplicate": "Name with such name already exists.", "name-duplicate": "Name with such name already exists.",
@ -1181,7 +1200,9 @@
"max-geofencing-zone": "Maximum number of geofencing zones reached.", "max-geofencing-zone": "Maximum number of geofencing zones reached.",
"zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.", "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.",
"zone-group-refresh-interval-required": "Zone groups refresh interval is required.", "zone-group-refresh-interval-required": "Zone groups refresh interval is required.",
"zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second." "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second.",
"propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.",
"data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data."
} }
}, },
"ai-models": { "ai-models": {

Loading…
Cancel
Save