Browse Source

Rule node test script dialog

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
642c9fabe6
  1. 2
      ui-ngx/proxy.conf.js
  2. 5
      ui-ngx/src/app/core/http/rule-chain.service.ts
  3. 93
      ui-ngx/src/app/core/services/script/node-script-test-dialog.component.html
  4. 93
      ui-ngx/src/app/core/services/script/node-script-test-dialog.component.scss
  5. 176
      ui-ngx/src/app/core/services/script/node-script-test-dialog.component.ts
  6. 51
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  7. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts
  8. 2
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  9. 12
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  10. 25
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  11. 1
      ui-ngx/src/app/shared/components/public-api.ts
  12. 7
      ui-ngx/src/app/shared/models/rule-node.models.ts

2
ui-ngx/proxy.conf.js

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

5
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<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>[] = [];

93
ui-ngx/src/app/core/services/script/node-script-test-dialog.component.html

@ -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>

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

176
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<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);
}
}

51
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<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);
}

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

2
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -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">

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

25
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<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.dialogRef.close(this.ruleNode);
this.ruleNodeDetailsComponent.validate();
if (this.ruleNodeDetailsComponent.ruleNodeFormGroup.valid) {
this.dialogRef.close(this.ruleNode);
}
}
}

1
ui-ngx/src/app/shared/components/public-api.ts

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

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

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

Loading…
Cancel
Save