Browse Source

Merge branch 'feature/calculated-fields' of github.com:thingsboard/thingsboard into feature/calculated-fields

pull/12681/head
Andrii Shvaika 1 year ago
parent
commit
722c707591
  1. 7
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  2. 62
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  3. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  4. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html
  5. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.scss
  6. 14
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts
  7. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  8. 29
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  9. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts
  10. 36
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html
  11. 88
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts
  12. 99
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html
  13. 73
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss
  14. 157
      ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.ts
  15. 2
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html
  16. 14
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  17. 10
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  18. 8
      ui-ngx/src/app/shared/components/value-input.component.html
  19. 4
      ui-ngx/src/app/shared/components/value-input.component.ts
  20. 20
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  21. 9
      ui-ngx/src/app/shared/models/entity.models.ts
  22. 7
      ui-ngx/src/app/shared/models/rule-node.models.ts
  23. 1
      ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md
  24. 1
      ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md
  25. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

7
ui-ngx/src/app/core/http/calculated-fields.service.ts

@ -19,9 +19,10 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { CalculatedField } from '@shared/models/calculated-field.models'; import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -48,4 +49,8 @@ export class CalculatedFieldsService {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config));
}
} }

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

@ -34,15 +34,20 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component'; import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators'; import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import { import {
CalculatedField, CalculatedField,
CalculatedFieldDebugDialogData, CalculatedFieldDebugDialogData,
CalculatedFieldDialogData CalculatedFieldDialogData,
CalculatedFieldTestScriptDialogData,
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { CalculatedFieldDebugDialogComponent, CalculatedFieldDialogComponent } from './components/public-api'; import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldDialogComponent,
CalculatedFieldScriptTestDialogComponent
} from './components/public-api';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; import { isObject } from '@core/utils';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
@ -53,7 +58,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
readonly tenantId = getCurrentAuthUser(this.store).tenantId; readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = { additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'), title: this.translate.instant('calculated-fields.see-debug-events'),
action: (id?: CalculatedFieldId) => this.openDebugDialog.call(this, id), action: (calculatedField: CalculatedField) => this.openDebugDialog.call(this, calculatedField),
}; };
constructor(private calculatedFieldsService: CalculatedFieldsService, constructor(private calculatedFieldsService: CalculatedFieldsService,
@ -134,10 +139,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
} }
onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void { onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = { const additionalActionConfig = {
...this.additionalDebugActionConfig, ...this.additionalDebugActionConfig,
action: () => this.openDebugDialog(id) action: () => this.openDebugDialog(calculatedField)
}; };
const { viewContainerRef } = this.getTable(); const { viewContainerRef } = this.getTable();
if ($event) { if ($event) {
@ -173,8 +179,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
) )
} }
private editCalculatedField(calculatedField: CalculatedField): void { private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
this.getCalculatedFieldDialog(calculatedField, 'action.apply') this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
.pipe( .pipe(
filter(Boolean), filter(Boolean),
switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })), switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })),
@ -186,7 +192,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}); });
} }
private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add'): Observable<CalculatedField> { private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, { return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
@ -198,19 +204,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
tenantId: this.tenantId, tenantId: this.tenantId,
entityName: this.entityName, entityName: this.entityName,
additionalDebugActionConfig: this.additionalDebugActionConfig, additionalDebugActionConfig: this.additionalDebugActionConfig,
} getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty
},
enterAnimationDuration: isDirty ? 0 : null,
}) })
.afterClosed(); .afterClosed();
} }
private openDebugDialog(id: CalculatedFieldId): void { private openDebugDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, { this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
tenantId: this.tenantId, tenantId: this.tenantId,
entityId: this.entityId, value: calculatedField,
id getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
} }
}) })
.afterClosed() .afterClosed()
@ -251,4 +260,29 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData()); ).subscribe(() => this.updateData());
} }
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, openCalculatedFieldEdit = true): Observable<string> {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) ? argumentsObj[key] : '';
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,
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({...calculatedField, configuration: {...calculatedField.configuration, expression } }, true)
}
}),
);
}
} }

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html

@ -24,7 +24,7 @@
<div class="tb-form-table-header-cell w-1/6">{{ 'entity.key' | translate }}</div> <div class="tb-form-table-header-cell w-1/6">{{ 'entity.key' | translate }}</div>
<div class="tb-form-table-header-cell w-24 min-w-24"></div> <div class="tb-form-table-header-cell w-24 min-w-24"></div>
</div> </div>
<div class="tb-form-table-body tb-drop-list"> <div class="tb-form-table-body">
@for (group of argumentsFormArray.controls; track group) { @for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row"> <div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="debug-dialog-container w-screen max-w-5xl"> <div class="w-screen max-w-5xl">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'calculated-fields.debugging' | translate}}</h2> <h2>{{ 'calculated-fields.debugging' | translate}}</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
@ -25,15 +25,16 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content class="tb-form-panel stroked debug-dialog-content h-full"> <div mat-dialog-content class="tb-form-panel stroked debug-dialog-content">
<tb-event-table <tb-event-table
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]" [debugEventTypes]="[debugEventTypes.DEBUG_CALCULATED_FIELD]"
[disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]" [disabledEventTypes]="[EventType.LC_EVENT, EventType.ERROR, EventType.STATS]"
[defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD" [defaultEventType]="DebugEventType.DEBUG_CALCULATED_FIELD"
[active]="true" [active]="true"
[entityId]="data.id" [entityId]="data.value.id"
[functionTestButtonLabel]="'common.test-function' | translate" [functionTestButtonLabel]="'common.test-function' | translate"
(debugEventSelected)="onDebugEventSelected($event)"
/> />
</div> </div>
<div mat-dialog-actions class="justify-end"> <div mat-dialog-actions class="justify-end">

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

@ -14,12 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.debug-dialog-container { .debug-dialog-content {
height: 77vh; height: 65vh;
min-width: 80vw; border-radius: 0;
.debug-dialog-content {
border-radius: 0;
}
} }
} }

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

@ -20,16 +20,16 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import { EventTableComponent } from '@home/components/event/event-table.component'; import { EventTableComponent } from '@home/components/event/event-table.component';
import { CalculatedFieldDebugDialogData } from '@shared/models/calculated-field.models'; import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models';
@Component({ @Component({
selector: 'tb-calculated-field-debug-dialog', selector: 'tb-calculated-field-debug-dialog',
styleUrls: ['calculated-field-debug-dialog.component.scss'], styleUrls: ['calculated-field-debug-dialog.component.scss'],
templateUrl: './calculated-field-debug-dialog.component.html', templateUrl: './calculated-field-debug-dialog.component.html',
}) })
export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, null> implements AfterViewInit { export class CalculatedFieldDebugDialogComponent extends DialogComponent<CalculatedFieldDebugDialogComponent, string> implements AfterViewInit {
@ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent; @ViewChild(EventTableComponent, {static: true}) eventsTable: EventTableComponent;
@ -40,15 +40,21 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, null>) { protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) {
super(store, router, dialogRef); super(store, router, dialogRef);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.eventsTable.entitiesTable.updateData(); this.eventsTable.entitiesTable.updateData();
this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = () => this.data.value.type === CalculatedFieldType.SCRIPT;
} }
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
onDebugEventSelected(event: CalculatedFieldEventBody): void {
this.data.getTestScriptDialogFn(this.data.value, JSON.parse(event.arguments))
.subscribe(expression => this.dialogRef.close(expression));
}
} }

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

@ -99,8 +99,16 @@
[functionArgs]="functionArgs$ | async" [functionArgs]="functionArgs$ | async"
[disableUndefinedCheck]="true" [disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL" [scriptLanguage]="ScriptLanguage.TBEL"
helpId="[TODO]: [Calculated Fields] add valid link" helpId="calculated-field/expression_fn"
/> />
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="configFormGroup.get('expressionSCRIPT').invalid || configFormGroup.get('arguments').invalid ">
{{ 'common.test-function' | translate }}
</button>
</div>
} }
</div> </div>
<div class="tb-form-panel" [formGroup]="outputFormGroup"> <div class="tb-form-panel" [formGroup]="outputFormGroup">

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject } from '@angular/core'; import { AfterViewInit, Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -41,7 +41,7 @@ import { ScriptLanguage } from '@shared/models/rule-node.models';
selector: 'tb-calculated-field-dialog', selector: 'tb-calculated-field-dialog',
templateUrl: './calculated-field-dialog.component.html', templateUrl: './calculated-field-dialog.component.html',
}) })
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> { export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> implements AfterViewInit {
fieldFormGroup = this.fb.group({ fieldFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]], name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
@ -67,7 +67,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
additionalDebugActionConfig = this.data.value?.id ? { additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig, ...this.data.additionalDebugActionConfig,
action: () => this.data.additionalDebugActionConfig.action(this.data.value.id) action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null; } : null;
readonly OutputTypeTranslations = OutputTypeTranslations; readonly OutputTypeTranslations = OutputTypeTranslations;
@ -98,18 +98,35 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
return this.fieldFormGroup.get('configuration').get('output') as FormGroup; return this.fieldFormGroup.get('configuration').get('output') as FormGroup;
} }
get fromGroupValue(): CalculatedField {
const { configuration, type, ...rest } = this.fieldFormGroup.value;
const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration;
return { configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField;
}
ngAfterViewInit(): void {
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
add(): void { add(): void {
if (this.fieldFormGroup.valid) { if (this.fieldFormGroup.valid) {
const { configuration, type, ...rest } = this.fieldFormGroup.value; this.dialogRef.close(this.fromGroupValue);
const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration;
this.dialogRef.close({ configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField);
} }
} }
onTestScript(): void {
this.data.getTestScriptDialogFn(this.fromGroupValue, null, false).subscribe(expression => {
this.configFormGroup.get('expressionSCRIPT').setValue(expression);
this.configFormGroup.get('expressionSCRIPT').markAsDirty();
});
}
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {}; const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {};
const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration; const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration;

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

@ -18,3 +18,4 @@ export * from './dialog/calculated-field-dialog.component';
export * from './arguments-table/calculated-field-arguments-table.component'; export * from './arguments-table/calculated-field-arguments-table.component';
export * from './panel/calculated-field-argument-panel.component'; export * from './panel/calculated-field-argument-panel.component';
export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './debug-dialog/calculated-field-debug-dialog.component';
export * from './test-dialog/calculated-field-script-test-dialog.component';

36
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html

@ -0,0 +1,36 @@
<!--
Copyright © 2016-2024 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="flex flex-col gap-2 px-4">
<div>{{ 'calculated-fields.arguments' | translate }}</div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell w-1/4">{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell flex-1">{{ 'common.value' | translate }}</div>
</div>
<div class="tb-form-table-body">
@for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="tb-inline-field w-1/4" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field>
<tb-value-input class="flex-1" [required]="false" formControlName="value"/>
</div>
}
</div>
</div>
</div>

88
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.ts

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2024 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 } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
ValidationErrors,
FormBuilder,
FormGroup
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-calculated-field-test-arguments',
templateUrl: './calculated-field-test-arguments.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CalculatedFieldTestArgumentsComponent),
multi: true,
}
]
})
export class CalculatedFieldTestArgumentsComponent extends PageComponent implements ControlValueAccessor, Validator {
argumentsFormArray = this.fb.array<FormGroup>([]);
private propagateChange: (value: { argumentName: string; value: unknown }) => void;
constructor(private fb: FormBuilder) {
super();
this.argumentsFormArray.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(() => this.propagateChange(this.getValue()));
}
registerOnChange(propagateChange: (value: { argumentName: string; value: unknown }) => void): void {
this.propagateChange = propagateChange;
}
registerOnTouched(_): void {
}
writeValue(argumentsObj: Record<string, unknown>): void {
this.argumentsFormArray.clear();
Object.keys(argumentsObj).forEach(key => {
this.argumentsFormArray.push(this.fb.group({
argumentName: [{ value: key, disabled: true}],
value: [argumentsObj[key]]
}) as FormGroup, {emitEvent: false});
});
}
validate(): ValidationErrors | null {
return this.argumentsFormArray.valid ? null : { arguments: { valid: false } };
}
private getValue(): { argumentName: string; value: unknown } {
return this.argumentsFormArray.getRawValue().reduce((acc, rowItem) => {
const { argumentName, value } = rowItem;
acc[argumentName] = value;
return acc;
}, {}) as { argumentName: string; value: unknown };
}
}

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

@ -0,0 +1,99 @@
<!--
Copyright © 2016-2024 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.
-->
<form class="test-dialog-container size-full" [formGroup]="calculatedFieldScriptTestFormGroup">
<mat-toolbar class="flex justify-between" color="primary">
<h2>{{ 'calculated-fields.test-script-function' | translate }} ({{ 'api-usage.tbel' | translate }})</h2>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content class="relative">
<div class="tb-absolute-fill">
<div class="tb-fullscreen-panel flex size-full flex-row">
<div #leftPanel class="test-block-content overflow-hidden">
<div class="relative size-full min-w-64">
<div class="block-label-container left">
<span class="block-label">{{ 'calculated-fields.expression' | translate }}</span>
</div>
<tb-js-func
#expressionContent
formControlName="expression"
functionName="calculate"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[fillHeight]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
resultType="object"
helpId="calculated-field/test-expression_fn"
/>
</div>
</div>
<div #rightPanel>
<div #topRightPanel class="test-block-content">
<div class="relative flex size-full min-w-96 gap-2">
<div class="block-label-container right-top">
<span class="block-label">{{ 'calculated-fields.arguments' | translate }}</span>
</div>
<tb-calculated-field-test-arguments class="size-full" formControlName="arguments"/>
</div>
</div>
<div #bottomRightPanel class="test-block-content">
<div class="relative size-full">
<div class="block-label-container right-bottom">
<span class="block-label" translate>common.output</span>
</div>
<tb-json-content
class="flex-1"
formControlName="output"
label="{{ 'common.output' | translate }}"
[contentType]="ContentType.JSON"
validateContent="false"
readonly="true"
[fillHeight]="true"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div mat-dialog-actions class="flex flex-row">
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="(isLoading$ | async) || calculatedFieldScriptTestFormGroup.invalid">
{{ 'action.test' | translate }}
</button>
<span class="flex-1"></span>
<button mat-button color="primary"
type="button"
cdkFocusInitial
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
(click)="save()"
[disabled]="(isLoading$ | async) || calculatedFieldScriptTestFormGroup.get('expression').invalid || !calculatedFieldScriptTestFormGroup.get('expression').dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

73
ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.scss

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2024 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.
*/
:host {
.test-dialog-container {
.block-label {
padding: 4px;
color: #00acc1;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
}
.test-block-content {
padding-top: 5px;
padding-left: 5px;
border: 1px solid #c0c0c0;
}
.block-label-container {
position: absolute;
z-index: 10;
font-size: 12px;
font-weight: bold;
&.left {
right: 112px;
top: 9px;
}
&.right-bottom {
right: 40px;
top: 6px;
}
&.right-top {
right: 8px;
top: 2px;
}
}
}
}
:host::ng-deep {
.test-dialog-container {
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-horizontal {
cursor: col-resize;
background-image: url("../../../../../../../assets/split.js/grips/horizontal.png");
}
.gutter.gutter-vertical {
cursor: row-resize;
background-image: url("../../../../../../../assets/split.js/grips/vertical.png");
}
}
}

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

@ -0,0 +1,157 @@
///
/// Copyright © 2016-2024 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 {
AfterViewInit,
Component,
DestroyRef,
ElementRef,
Inject,
ViewChild,
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder } from '@angular/forms';
import { NEVER, Observable, of, switchMap } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { ContentType } from '@shared/models/constants';
import { JsonContentComponent } from '@shared/components/json-content.component';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { beautifyJs } from '@shared/models/beautify.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs/operators';
import { CalculatedFieldTestScriptDialogData } from '@shared/models/calculated-field.models';
@Component({
selector: 'tb-calculated-field-script-test-dialog',
templateUrl: './calculated-field-script-test-dialog.component.html',
styleUrls: ['./calculated-field-script-test-dialog.component.scss'],
})
export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<CalculatedFieldScriptTestDialogComponent,
string> implements AfterViewInit {
@ViewChild('leftPanel', {static: true}) leftPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('rightPanel', {static: true}) rightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('topRightPanel', {static: true}) topRightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('bottomRightPanel', {static: true}) bottomRightPanelElmRef: ElementRef<HTMLElement>;
@ViewChild('expressionContent', {static: true}) expressionContent: JsonContentComponent;
calculatedFieldScriptTestFormGroup = this.fb.group({
expression: [],
arguments: [],
output: []
});
readonly ContentType = ContentType;
readonly ScriptLanguage = ScriptLanguage;
readonly functionArgs = Object.keys(this.data.arguments);
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldTestScriptDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldScriptTestDialogComponent, string>,
private dialog: MatDialog,
private fb: FormBuilder,
private destroyRef: DestroyRef,
private calculatedFieldService: CalculatedFieldsService) {
super(store, router, dialogRef);
beautifyJs(this.data.expression, {indent_size: 4}).pipe(filter(Boolean), takeUntilDestroyed()).subscribe(
(res) => this.calculatedFieldScriptTestFormGroup.get('expression').patchValue(res, {emitEvent: false})
);
this.calculatedFieldScriptTestFormGroup.get('arguments').patchValue(this.data.arguments, {emitEvent: false});
}
ngAfterViewInit(): void {
this.initSplitLayout(
this.leftPanelElmRef.nativeElement,
this.rightPanelElmRef.nativeElement,
this.topRightPanelElmRef.nativeElement,
this.bottomRightPanelElmRef.nativeElement
);
}
cancel(): void {
this.dialogRef.close(null);
}
onTestScript(): void {
this.testScript()
.pipe(
switchMap(output => beautifyJs(output, {indent_size: 4})),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(output => this.calculatedFieldScriptTestFormGroup.get('output').setValue(output));
}
save(): void {
this.testScript(true).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.calculatedFieldScriptTestFormGroup.get('expression').markAsPristine();
this.dialogRef.close(this.calculatedFieldScriptTestFormGroup.get('expression').value);
});
}
private testScript(onSave = false): Observable<string> {
if (this.checkInputParamErrors()) {
return this.calculatedFieldService.testScript({
expression: this.calculatedFieldScriptTestFormGroup.get('expression').value,
arguments: this.calculatedFieldScriptTestFormGroup.get('arguments').value
}).pipe(
switchMap(result => {
if (result.error) {
this.store.dispatch(new ActionNotificationShow(
{
message: result.error,
type: 'error'
}));
return NEVER;
} else {
if (onSave && this.data.openCalculatedFieldEdit) {
this.dialog.closeAll();
}
return of(result.output);
}
}),
);
} else {
return NEVER;
}
}
private checkInputParamErrors(): boolean {
this.expressionContent.validateOnSubmit();
return !this.calculatedFieldScriptTestFormGroup.get('expression').invalid;
}
private initSplitLayout(leftPanel, rightPanel, topRightPanel, bottomRightPanel): void {
Split([leftPanel, rightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize'
});
Split([topRightPanel, bottomRightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical'
});
}
}

2
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html

@ -54,7 +54,7 @@
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"
(click)="additionalActionConfig.action()"> (click)="onCancel(); additionalActionConfig.action()">
{{ additionalActionConfig.title | translate }} {{ additionalActionConfig.title | translate }}
</button> </button>
} }

14
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -357,10 +357,10 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
break; break;
case DebugEventType.DEBUG_CALCULATED_FIELD: case DebugEventType.DEBUG_CALCULATED_FIELD:
this.columns[0].width = '80px'; this.columns[0].width = '80px';
this.columns[1].width = '20%'; this.columns[1].width = '100px';
this.columns.push( this.columns.push(
new EntityTableColumn<Event>('entityId', 'event.entity-id', '85px', new EntityTableColumn<Event>('entityId', 'event.entity-id', '85px',
(entity) => `<span style="display: inline-block; width: 7ch">${entity.body.entityId.substring(0, 6)}…</span>`, (entity) => `<span style="display: inline-block; width: 9ch">${entity.body.entityId.substring(0, 8)}…</span>`,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
false, false,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
@ -380,7 +380,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
} }
), ),
new EntityTableColumn<Event>('messageId', 'event.message-id', '85px', new EntityTableColumn<Event>('messageId', 'event.message-id', '85px',
(entity) => `<span style="display: inline-block; width: 7ch">${entity.body.msgId?.substring(0, 6)}…</span>`, (entity) => `<span style="display: inline-block; width: 9ch">${entity.body.msgId?.substring(0, 8)}…</span>`,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
false, false,
() => ({padding: '0 12px 0 0'}), () => ({padding: '0 12px 0 0'}),
@ -457,6 +457,14 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}); });
} }
break; break;
case DebugEventType.DEBUG_CALCULATED_FIELD:
this.cellActionDescriptors.push({
name: this.translate.instant('common.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report',
isEnabled: () => true,
onAction: (_, entity) => this.debugEventSelected.next(entity.body)
});
break;
} }
this.getTable()?.cellActionDescriptorsUpdated(); this.getTable()?.cellActionDescriptorsUpdated();
} }

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

@ -198,6 +198,12 @@ import {
import { import {
CalculatedFieldDebugDialogComponent CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component'; } 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';
@NgModule({ @NgModule({
declarations: declarations:
@ -347,6 +353,8 @@ import {
CalculatedFieldArgumentsTableComponent, CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent, CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -490,6 +498,8 @@ import {
CalculatedFieldArgumentsTableComponent, CalculatedFieldArgumentsTableComponent,
CalculatedFieldArgumentPanelComponent, CalculatedFieldArgumentPanelComponent,
CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogComponent,
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestArgumentsComponent,
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

8
ui-ngx/src/app/shared/components/value-input.component.html

@ -32,7 +32,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.STRING" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1"> <mat-form-field *ngIf="valueType === valueTypeEnum.STRING" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput required name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" <input [disabled]="disabled" matInput [required]="required" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.string-value' | translate }}*"/> placeholder="{{ 'value.string-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
@ -44,7 +44,7 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1"> <mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1">
<input [disabled]="disabled" matInput required name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" <input [disabled]="disabled" matInput [required]="required" name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.integer-value' | translate }}*"/> placeholder="{{ 'value.integer-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
@ -57,7 +57,7 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1"> <mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1">
<input [disabled]="disabled" matInput required name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" <input [disabled]="disabled" matInput [required]="required" name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.double-value' | translate }}*"/> placeholder="{{ 'value.double-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
@ -80,7 +80,7 @@
</ng-container> </ng-container>
<div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2"> <div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1"> <mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel" <input [disabled]="disabled" matInput tb-json-to-string [required]="required" name="value" #value="ngModel"
[(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" placeholder="{{ 'value.json-value' | translate }}*"/> [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" placeholder="{{ 'value.json-value' | translate }}*"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"

4
ui-ngx/src/app/shared/components/value-input.component.ts

@ -81,6 +81,10 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
@coerceBoolean() @coerceBoolean()
shortBooleanField = false; shortBooleanField = false;
@Input()
@coerceBoolean()
required = true;
@Input() @Input()
layout: ValueInputLayout | Layout = 'row'; layout: ValueInputLayout | Layout = 'row';

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

@ -26,6 +26,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType } from '@shared/models/alias.models'; import { AliasFilterType } from '@shared/models/alias.models';
import { Observable } from 'rxjs';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> { export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
debugSettings?: EntityDebugSettings; debugSettings?: EntityDebugSettings;
@ -126,6 +127,8 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument {
argumentName: string; argumentName: string;
} }
export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;
export interface CalculatedFieldDialogData { export interface CalculatedFieldDialogData {
value?: CalculatedField; value?: CalculatedField;
buttonTitle: string; buttonTitle: string;
@ -133,13 +136,24 @@ export interface CalculatedFieldDialogData {
debugLimitsConfiguration: string; debugLimitsConfiguration: string;
tenantId: string; tenantId: string;
entityName?: string; entityName?: string;
additionalDebugActionConfig: AdditionalDebugActionConfig; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
isDirty?: boolean;
} }
export interface CalculatedFieldDebugDialogData { export interface CalculatedFieldDebugDialogData {
id?: CalculatedFieldId;
entityId: EntityId;
tenantId: string; tenantId: string;
value: CalculatedField;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
}
export interface CalculatedFieldTestScriptInputParams {
arguments: Record<string, unknown>,
expression: string;
}
export interface CalculatedFieldTestScriptDialogData extends CalculatedFieldTestScriptInputParams {
openCalculatedFieldEdit?: boolean;
} }
export interface ArgumentEntityTypeParams { export interface ArgumentEntityTypeParams {

9
ui-ngx/src/app/shared/models/entity.models.ts

@ -203,8 +203,13 @@ export interface EntityDebugSettings {
allEnabledUntil?: number; allEnabledUntil?: number;
} }
export interface AdditionalDebugActionConfig { export interface EntityTestScriptResult {
action?: (id?: EntityId) => void; output: string;
error: string;
}
export interface AdditionalDebugActionConfig<Action = (...args: unknown[]) => void> {
action: Action;
title: string; title: string;
} }

7
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -27,7 +27,7 @@ import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { HasEntityDebugSettings } from '@shared/models/entity.models'; import { EntityTestScriptResult, HasEntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface RuleNodeConfiguration { export interface RuleNodeConfiguration {
@ -374,10 +374,7 @@ export interface TestScriptInputParams {
msgType: string; msgType: string;
} }
export interface TestScriptResult { export type TestScriptResult = EntityTestScriptResult;
output: string;
error: string;
}
export enum MessageType { export enum MessageType {
POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST', POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST',

1
ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md

@ -0,0 +1 @@
<!-- [TODO]: [Calculated Fields] add content -->

1
ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md

@ -0,0 +1 @@
<!-- [TODO]: [Calculated Fields] add content -->

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

@ -65,6 +65,7 @@
"next-with-label": "Next: {{label}}", "next-with-label": "Next: {{label}}",
"read-more": "Read more", "read-more": "Read more",
"hide": "Hide", "hide": "Hide",
"test": "Test",
"done": "Done", "done": "Done",
"print": "Print", "print": "Print",
"restore": "Restore", "restore": "Restore",
@ -1018,6 +1019,7 @@
"argument-name": "Argument name", "argument-name": "Argument name",
"datasource": "Datasource", "datasource": "Datasource",
"add-argument": "Add argument", "add-argument": "Add argument",
"test-script-function": "Test script 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",
@ -1107,8 +1109,12 @@
"proceed": "Proceed", "proceed": "Proceed",
"open-details-page": "Open details page", "open-details-page": "Open details page",
"not-found": "Not found", "not-found": "Not found",
"value": "Value",
"documentation": "Documentation", "documentation": "Documentation",
"time-left": "{{time}} left", "time-left": "{{time}} left",
"output": "Output",
"test-function": "Test function",
"test-with-this-message": "{{test}} with this message",
"suffix": { "suffix": {
"s": "s", "s": "s",
"ms": "ms" "ms": "ms"

Loading…
Cancel
Save