From 642c9fabe6d91379b020a843841354fd5226af58 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 24 Dec 2019 13:54:00 +0200 Subject: [PATCH] Rule node test script dialog --- ui-ngx/proxy.conf.js | 2 +- .../src/app/core/http/rule-chain.service.ts | 5 + .../node-script-test-dialog.component.html | 93 +++++++++ .../node-script-test-dialog.component.scss | 93 +++++++++ .../node-script-test-dialog.component.ts | 176 ++++++++++++++++++ .../script/node-script-test.service.ts | 51 ++++- .../rulechain/rule-node-config.component.ts | 6 + .../rule-node-details.component.html | 2 +- .../rulechain/rule-node-details.component.ts | 12 +- .../rulechain/rulechain-page.component.ts | 25 ++- .../src/app/shared/components/public-api.ts | 1 + .../src/app/shared/models/rule-node.models.ts | 7 + 12 files changed, 461 insertions(+), 12 deletions(-) create mode 100644 ui-ngx/src/app/core/services/script/node-script-test-dialog.component.html create mode 100644 ui-ngx/src/app/core/services/script/node-script-test-dialog.component.scss create mode 100644 ui-ngx/src/app/core/services/script/node-script-test-dialog.component.ts diff --git a/ui-ngx/proxy.conf.js b/ui-ngx/proxy.conf.js index 6b45570da1..f7d53fae70 100644 --- a/ui-ngx/proxy.conf.js +++ b/ui-ngx/proxy.conf.js @@ -15,7 +15,7 @@ */ const ruleNodeUiforwardHost = 'localhost'; -const ruleNodeUiforwardPort = 8080; +const ruleNodeUiforwardPort = 5000; const PROXY_CONFIG = { '/api': { diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index 4fe6b37683..1f9ade5601 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -38,6 +38,7 @@ import { catchError, map, mergeMap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { deepClone, snakeCase } from '@core/utils'; +import { DebugRuleNodeEventBody } from '@app/shared/models/event.models'; @Injectable({ providedIn: 'root' @@ -170,6 +171,10 @@ export class RuleChainService { return component.configurationDescriptor.nodeDefinition.customRelations; } + public getLatestRuleNodeDebugInput(ruleNodeId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); + } + private resolveTargetRuleChains(ruleChainConnections: Array): Observable<{[ruleChainId: string]: RuleChain}> { if (ruleChainConnections && ruleChainConnections.length) { const tasks: Observable[] = []; diff --git a/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.html b/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.html new file mode 100644 index 0000000000..8a3e17247b --- /dev/null +++ b/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.html @@ -0,0 +1,93 @@ + +
+ +

{{ 'rulenode.test-script-function' | translate }}

+ + +
+ + +
+
+
+
+
+
+
+ +
+ TODO: payloadForm +
+
+
+
+
+ + TODO: metadataForm +
+
+
+
+
+
+
+
+ +
+ TODO: funcBodyForm +
+
+
+
+
+ +
+ TODO: output +
+
+
+
+
+
+ + + + +
+
diff --git a/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.scss b/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.scss new file mode 100644 index 0000000000..e848572f2f --- /dev/null +++ b/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.scss @@ -0,0 +1,93 @@ +:host { + .tb-split { + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + } + + .tb-content { + padding-top: 5px; + padding-left: 5px; + border: 1px solid #c0c0c0; + } + + .gutter { + background-color: #eee; + + background-repeat: no-repeat; + background-position: 50%; + } + + .gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; + } + + .gutter.gutter-horizontal { + cursor: col-resize; + background-image: url("../../../../assets/split.js/grips/vertical.png"); + } + + .gutter.gutter-vertical { + cursor: row-resize; + background-image: url("../../../../assets/split.js/grips/horizontal.png"); + } + + .tb-split.tb-split-horizontal, + .gutter.gutter-horizontal { + float: left; + height: 100%; + } + + .tb-split.tb-split-vertical { + display: flex; + + .tb-split.tb-content { + height: 100%; + } + } + + div.tb-editor-area-title-panel { + position: absolute; + top: 13px; + right: 40px; + z-index: 5; + font-size: .8rem; + font-weight: 500; + + &.tb-js-function { + right: 80px; + } + + label { + padding: 4px; + color: #00acc1; + text-transform: uppercase; + background: rgba(220, 220, 220, .35); + border-radius: 5px; + } + + .mat-button { + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + } + } + + .tb-resize-container { + position: relative; + width: 100%; + height: 100%; + overflow-y: auto; + + .ace_editor { + height: 100%; + } + } +} diff --git a/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.ts b/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.ts new file mode 100644 index 0000000000..d5e4cb28f8 --- /dev/null +++ b/ui-ngx/src/app/core/services/script/node-script-test-dialog.component.ts @@ -0,0 +1,176 @@ +/// +/// Copyright © 2016-2019 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, + ElementRef, + Inject, + OnInit, + QueryList, + SkipSelf, + ViewChild, + ViewChildren +} from '@angular/core'; +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { combineLatest, Observable, of } from 'rxjs'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + toCustomAction, + WidgetActionCallbacks, + WidgetActionDescriptorInfo, + WidgetActionsData +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { UtilsService } from '@core/services/utils.service'; +import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; + +export interface NodeScriptTestDialogData { + script: string; + scriptType: string; + functionTitle: string; + functionName: string; + argNames: string[]; + msg?: any; + metadata?: any; + msgType?: string; +} + +@Component({ + selector: 'tb-node-script-test-dialog', + templateUrl: './node-script-test-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: NodeScriptTestDialogComponent}], + styleUrls: ['./node-script-test-dialog.component.scss'] +}) +export class NodeScriptTestDialogComponent extends DialogComponent implements OnInit, AfterViewInit, ErrorStateMatcher { + + @ViewChildren('topPanel') + topPanelElmRef: QueryList>; + + @ViewChildren('topLeftPanel') + topLeftPanelElmRef: QueryList>; + + @ViewChildren('topRightPanel') + topRightPanelElmRef: QueryList>; + + @ViewChildren('bottomPanel') + bottomPanelElmRef: QueryList>; + + @ViewChildren('bottomLeftPanel') + bottomLeftPanelElmRef: QueryList>; + + @ViewChildren('bottomLeftPanel') + bottomRightPanelElmRef: QueryList>; + + nodeScriptTestFormGroup: FormGroup; + + functionTitle: string; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.functionTitle = this.data.functionTitle; + } + + ngOnInit(): void { + this.nodeScriptTestFormGroup = this.fb.group({}); + } + + ngAfterViewInit(): void { + combineLatest(this.topPanelElmRef.changes, + this.topLeftPanelElmRef.changes, + this.topRightPanelElmRef.changes, + this.bottomPanelElmRef.changes, + this.bottomLeftPanelElmRef.changes, + this.bottomRightPanelElmRef.changes).subscribe(() => { + if (this.topPanelElmRef.length && this.topLeftPanelElmRef.length && + this.topRightPanelElmRef.length && this.bottomPanelElmRef.length && + this.bottomLeftPanelElmRef.length && this.bottomRightPanelElmRef.length) { + this.initSplitLayout(this.topPanelElmRef.first.nativeElement, + this.topLeftPanelElmRef.first.nativeElement, + this.topRightPanelElmRef.first.nativeElement, + this.bottomPanelElmRef.first.nativeElement, + this.bottomLeftPanelElmRef.first.nativeElement, + this.bottomRightPanelElmRef.first.nativeElement); + } + }); + } + + private initSplitLayout(topPanel: any, + topLeftPanel: any, + topRightPanel: any, + bottomPanel: any, + bottomLeftPanel: any, + bottomRightPanel: any) { + + Split([topPanel, bottomPanel], { + sizes: [35, 65], + gutterSize: 8, + cursor: 'row-resize', + direction: 'vertical' + }); + + Split([topLeftPanel, topRightPanel], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + + Split([bottomLeftPanel, bottomRightPanel], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize' + }); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + const script: string = this.nodeScriptTestFormGroup.get('funcBody').value; + this.dialogRef.close(script); + } +} diff --git a/ui-ngx/src/app/core/services/script/node-script-test.service.ts b/ui-ngx/src/app/core/services/script/node-script-test.service.ts index 5391c2d602..3efa935c2e 100644 --- a/ui-ngx/src/app/core/services/script/node-script-test.service.ts +++ b/ui-ngx/src/app/core/services/script/node-script-test.service.ts @@ -16,14 +16,63 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { map, switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class NodeScriptTestService { - testNodeScript(script: string, scriptType: any, functionTitle: string, + constructor(private ruleChainService: RuleChainService) { + } + + testNodeScript(script: string, scriptType: string, functionTitle: string, functionName: string, argNames: string[], ruleNodeId: string): Observable { + if (ruleNodeId) { + return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( + switchMap((debugIn) => { + let msg: any; + let metadata: any; + let msgType: string; + if (debugIn) { + if (debugIn.data) { + msg = JSON.parse(debugIn.data); + } + if (debugIn.metadata) { + metadata = JSON.parse(debugIn.metadata); + } + msgType = debugIn.msgType; + } + return this.openTestScriptDialog(script, scriptType, functionTitle, + functionName, argNames, msg, metadata, msgType); + }) + ); + } else { + return this.openTestScriptDialog(script, scriptType, functionTitle, + functionName, argNames); + } + } + + private openTestScriptDialog(script: string, scriptType: string, + functionTitle: string, functionName: string, argNames: string[], + msg?: any, metadata?: any, msgType?: string): Observable { + if (!msg) { + msg = { + temperature: 22.4, + humidity: 78 + }; + } + if (!metadata) { + metadata = { + deviceType: 'default', + deviceName: 'Test Device', + ts: new Date().getTime() + '' + }; + } + if (!msgType) { + msgType = 'POST_TELEMETRY_REQUEST'; + } console.log(`testNodeScript TODO: ${script}`); return of(script); } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts index 11ebc02284..180c97708f 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -205,4 +205,10 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On }); } } + + validate() { + if (this.useDefinedDirective()) { + this.definedConfigComponent.validate(); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index 6a5f070c8b..df44f40b19 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -31,7 +31,7 @@ {{ 'rulenode.debug-mode' | translate }} - diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 74b203ccd5..10f80c6dd1 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -24,6 +24,8 @@ import { RuleNodeType } from '@shared/models/rule-node.models'; import { EntityType } from '@shared/models/entity-type.models'; import { Subscription } from 'rxjs'; import { RuleChainService } from '@core/http/rule-chain.service'; +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { RuleNodeConfigComponent } from './rule-node-config.component'; @Component({ selector: 'tb-rule-node', @@ -34,6 +36,8 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O @ViewChild('ruleNodeForm', {static: true}) ruleNodeForm: NgForm; + @ViewChild('ruleNodeConfigComponent', {static: false}) ruleNodeConfigComponent: RuleNodeConfigComponent; + @Input() ruleNode: FcRuleNode; @@ -127,4 +131,10 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O } } } + + validate() { + if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { + this.ruleNodeConfigComponent.validate(); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index a7f4a6e92c..a4ef9a7d9d 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -67,6 +67,7 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { UtilsService } from '@core/services/utils.service'; import { EntityService } from '@core/http/entity.service'; import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component'; +import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; @Component({ selector: 'tb-rulechain-page', @@ -585,14 +586,17 @@ export class RuleChainPageComponent extends PageComponent } saveRuleNode() { - this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); - if (this.editingRuleNode.error) { - delete this.editingRuleNode.error; + this.ruleNodeComponent.validate(); + if (this.ruleNodeComponent.ruleNodeFormGroup.valid) { + this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); + if (this.editingRuleNode.error) { + delete this.editingRuleNode.error; + } + this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; + this.editingRuleNode = deepClone(this.editingRuleNode); + this.onModelChanged(); + this.updateRuleNodesHighlight(); } - this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; - this.editingRuleNode = deepClone(this.editingRuleNode); - this.onModelChanged(); - this.updateRuleNodesHighlight(); } saveRuleNodeLink() { @@ -938,6 +942,8 @@ export interface AddRuleNodeDialogData { export class AddRuleNodeDialogComponent extends DialogComponent implements OnInit, ErrorStateMatcher { + @ViewChild('tbRuleNode', {static: true}) ruleNodeDetailsComponent: RuleNodeDetailsComponent; + ruleNode: FcRuleNode; ruleChainId: string; @@ -973,6 +979,9 @@ export class AddRuleNodeDialogComponent extends DialogComponent; + validate(); [key: string]: any; } @@ -97,11 +98,17 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple this.onConfigurationSet(this.configuration); } + validate() { + this.onValidate(); + } + protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); protected notifyConfigurationUpdated(configuration: RuleNodeConfiguration) { this.configurationChangedEmiter.emit(configuration); } + + protected onValidate() {} }