Rule node test script dialog

This commit is contained in:
Igor Kulikov 2019-12-24 13:54:00 +02:00
parent c6c53fc77c
commit 642c9fabe6
12 changed files with 461 additions and 12 deletions

View File

@ -15,7 +15,7 @@
*/
const ruleNodeUiforwardHost = 'localhost';
const ruleNodeUiforwardPort = 8080;
const ruleNodeUiforwardPort = 5000;
const PROXY_CONFIG = {
'/api': {

View File

@ -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<DebugRuleNodeEventBody> {
return this.http.get<DebugRuleNodeEventBody>(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config));
}
private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> {
if (ruleChainConnections && ruleChainConnections.length) {
const tasks: Observable<RuleChain>[] = [];

View File

@ -0,0 +1,93 @@
<!--
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.
-->
<form #nodeScriptTestForm="ngForm" [formGroup]="nodeScriptTestFormGroup" (ngSubmit)="save()" style="width: 800px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ 'rulenode.test-script-function' | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content fxFlex style="position: relative;">
<div class="tb-absolute-fill">
<div #topPanel class="tb-split tb-split-vertical">
<div #topLeftPanel class="tb-split tb-content">
<div class="tb-resize-container">
<div class="tb-editor-area-title-panel">
<label translate>rulenode.message</label>
</div>
TODO: payloadForm
</div>
</div>
<div #topRightPanel class="tb-split tb-content">
<div tb-toast toastTarget="metadataPanel" class="tb-resize-container">
<div class="tb-editor-area-title-panel">
<label translate>rulenode.metadata</label>
TODO: metadataForm
</div>
</div>
</div>
</div>
<div #bottomPanel class="tb-split tb-split-vertical">
<div #bottomLeftPanel class="tb-split tb-content">
<div class="tb-resize-container">
<div class="tb-editor-area-title-panel tb-js-function">
<label>{{ functionTitle }}</label>
</div>
TODO: funcBodyForm
</div>
</div>
<div #bottomRightPanel class="tb-split tb-content">
<div class="tb-resize-container">
<div class="tb-editor-area-title-panel">
<label translate>rulenode.output</label>
</div>
TODO: output
</div>
</div>
</div>
</div>
</div>
<div mat-dialog-actions fxLayout="row">
<button mat-button mat-raised-button color="primary"
type="button"
(click)="test()"
cdkFocusInitial
[disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.invalid">
{{ 'rulenode.test' | translate }}
</button>
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('funcBody').invalid || !nodeScriptTestFormGroup.get('funcBody').dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

View File

@ -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%;
}
}
}

View File

@ -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<NodeScriptTestDialogComponent,
string> implements OnInit, AfterViewInit, ErrorStateMatcher {
@ViewChildren('topPanel')
topPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ViewChildren('topLeftPanel')
topLeftPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ViewChildren('topRightPanel')
topRightPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ViewChildren('bottomPanel')
bottomPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ViewChildren('bottomLeftPanel')
bottomLeftPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ViewChildren('bottomLeftPanel')
bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>;
nodeScriptTestFormGroup: FormGroup;
functionTitle: string;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>,
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);
}
}

View File

@ -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<string> {
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<string> {
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);
}

View File

@ -205,4 +205,10 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
});
}
}
validate() {
if (this.useDefinedDirective()) {
this.definedConfigComponent.validate();
}
}
}

View File

@ -31,7 +31,7 @@
{{ 'rulenode.debug-mode' | translate }}
</mat-checkbox>
</section>
<tb-rule-node-config
<tb-rule-node-config #ruleNodeConfigComponent
formControlName="configuration"
[ruleNodeId]="ruleNode.ruleNodeId?.id"
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition">

View File

@ -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();
}
}
}

View File

@ -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,6 +586,8 @@ export class RuleChainPageComponent extends PageComponent
}
saveRuleNode() {
this.ruleNodeComponent.validate();
if (this.ruleNodeComponent.ruleNodeFormGroup.valid) {
this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
if (this.editingRuleNode.error) {
delete this.editingRuleNode.error;
@ -594,6 +597,7 @@ export class RuleChainPageComponent extends PageComponent
this.onModelChanged();
this.updateRuleNodesHighlight();
}
}
saveRuleNodeLink() {
this.ruleNodeLinkComponent.ruleNodeLinkFormGroup.markAsPristine();
@ -938,6 +942,8 @@ export interface AddRuleNodeDialogData {
export class AddRuleNodeDialogComponent extends DialogComponent<AddRuleNodeDialogComponent, FcRuleNode>
implements OnInit, ErrorStateMatcher {
@ViewChild('tbRuleNode', {static: true}) ruleNodeDetailsComponent: RuleNodeDetailsComponent;
ruleNode: FcRuleNode;
ruleChainId: string;
@ -973,6 +979,9 @@ export class AddRuleNodeDialogComponent extends DialogComponent<AddRuleNodeDialo
add(): void {
this.submitted = true;
this.ruleNodeDetailsComponent.validate();
if (this.ruleNodeDetailsComponent.ruleNodeFormGroup.valid) {
this.dialogRef.close(this.ruleNode);
}
}
}

View File

@ -15,3 +15,4 @@
///
export * from './page.component';
export * from './js-func.component';

View File

@ -78,6 +78,7 @@ export interface IRuleNodeConfigurationComponent {
ruleNodeId: string;
configuration: RuleNodeConfiguration;
configurationChanged: Observable<RuleNodeConfiguration>;
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() {}
}