25 changed files with 622 additions and 55 deletions
@ -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> |
|||
@ -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 }; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
@ -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' |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
<!-- [TODO]: [Calculated Fields] add content --> |
|||
@ -0,0 +1 @@ |
|||
<!-- [TODO]: [Calculated Fields] add content --> |
|||
Loading…
Reference in new issue