Commit db02d42e3762e39f793f265ba64e081cc072a439

Authored by Igor Kulikov
1 parent 8e37e283

Rule Node Test Script

@@ -31,7 +31,7 @@ import { ComponentDescriptorService } from './component-descriptor.service'; @@ -31,7 +31,7 @@ import { ComponentDescriptorService } from './component-descriptor.service';
31 import { 31 import {
32 IRuleNodeConfigurationComponent, 32 IRuleNodeConfigurationComponent,
33 LinkLabel, 33 LinkLabel,
34 - RuleNodeComponentDescriptor 34 + RuleNodeComponentDescriptor, TestScriptInputParams, TestScriptResult
35 } from '@app/shared/models/rule-node.models'; 35 } from '@app/shared/models/rule-node.models';
36 import { ResourcesService } from '../services/resources.service'; 36 import { ResourcesService } from '../services/resources.service';
37 import { catchError, map, mergeMap } from 'rxjs/operators'; 37 import { catchError, map, mergeMap } from 'rxjs/operators';
@@ -175,6 +175,10 @@ export class RuleChainService { @@ -175,6 +175,10 @@ export class RuleChainService {
175 return this.http.get<DebugRuleNodeEventBody>(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); 175 return this.http.get<DebugRuleNodeEventBody>(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config));
176 } 176 }
177 177
  178 + public testScript(inputParams: TestScriptInputParams, config?: RequestConfig): Observable<TestScriptResult> {
  179 + return this.http.post<TestScriptResult>('/api/ruleChain/testScript', inputParams, defaultHttpOptionsFromConfig(config));
  180 + }
  181 +
178 private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> { 182 private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> {
179 if (ruleChainConnections && ruleChainConnections.length) { 183 if (ruleChainConnections && ruleChainConnections.length) {
180 const tasks: Observable<RuleChain>[] = []; 184 const tasks: Observable<RuleChain>[] = [];
@@ -23,7 +23,7 @@ import { @@ -23,7 +23,7 @@ import {
23 HttpResponseBase 23 HttpResponseBase
24 } from '@angular/common/http'; 24 } from '@angular/common/http';
25 import { Observable } from 'rxjs/internal/Observable'; 25 import { Observable } from 'rxjs/internal/Observable';
26 -import { Injectable } from '@angular/core'; 26 +import { Inject, Injectable } from '@angular/core';
27 import { AuthService } from '../auth/auth.service'; 27 import { AuthService } from '../auth/auth.service';
28 import { Constants } from '../../shared/models/constants'; 28 import { Constants } from '../../shared/models/constants';
29 import { InterceptorHttpParams } from './interceptor-http-params'; 29 import { InterceptorHttpParams } from './interceptor-http-params';
@@ -52,10 +52,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor { @@ -52,10 +52,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
52 52
53 private activeRequests = 0; 53 private activeRequests = 0;
54 54
55 - constructor(private store: Store<AppState>,  
56 - private dialogService: DialogService,  
57 - private translate: TranslateService,  
58 - private authService: AuthService) { 55 + constructor(@Inject(Store) private store: Store<AppState>,
  56 + @Inject(DialogService) private dialogService: DialogService,
  57 + @Inject(TranslateService) private translate: TranslateService,
  58 + @Inject(AuthService) private authService: AuthService) {
59 } 59 }
60 60
61 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { 61 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
@@ -36,7 +36,7 @@ export class NodeScriptTestService { @@ -36,7 +36,7 @@ export class NodeScriptTestService {
36 return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( 36 return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
37 switchMap((debugIn) => { 37 switchMap((debugIn) => {
38 let msg: any; 38 let msg: any;
39 - let metadata: any; 39 + let metadata: {[key: string]: string};
40 let msgType: string; 40 let msgType: string;
41 if (debugIn) { 41 if (debugIn) {
42 if (debugIn.data) { 42 if (debugIn.data) {
@@ -59,7 +59,7 @@ export class NodeScriptTestService { @@ -59,7 +59,7 @@ export class NodeScriptTestService {
59 59
60 private openTestScriptDialog(script: string, scriptType: string, 60 private openTestScriptDialog(script: string, scriptType: string,
61 functionTitle: string, functionName: string, argNames: string[], 61 functionTitle: string, functionName: string, argNames: string[],
62 - msg?: any, metadata?: any, msgType?: string): Observable<string> { 62 + msg?: any, metadata?: {[key: string]: string}, msgType?: string): Observable<string> {
63 if (!msg) { 63 if (!msg) {
64 msg = { 64 msg = {
65 temperature: 22.4, 65 temperature: 22.4,
@@ -26,9 +26,6 @@ @@ -26,9 +26,6 @@
26 <mat-icon class="material-icons">close</mat-icon> 26 <mat-icon class="material-icons">close</mat-icon>
27 </button> 27 </button>
28 </mat-toolbar> 28 </mat-toolbar>
29 - <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">  
30 - </mat-progress-bar>  
31 - <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>  
32 <div mat-dialog-content fxFlex style="position: relative;"> 29 <div mat-dialog-content fxFlex style="position: relative;">
33 <div class="tb-absolute-fill"> 30 <div class="tb-absolute-fill">
34 <div #topPanel class="tb-split tb-split-vertical"> 31 <div #topPanel class="tb-split tb-split-vertical">
@@ -37,7 +34,24 @@ @@ -37,7 +34,24 @@
37 <div class="tb-editor-area-title-panel"> 34 <div class="tb-editor-area-title-panel">
38 <label translate>rulenode.message</label> 35 <label translate>rulenode.message</label>
39 </div> 36 </div>
40 - TODO: payloadForm 37 + <div formGroupName="payload" fxLayout="column" style="height: 100%;">
  38 + <div fxLayout="row">
  39 + <tb-message-type-autocomplete
  40 + style="margin-bottom: 0px; min-width: 300px;"
  41 + formControlName="msgType"
  42 + required>
  43 + </tb-message-type-autocomplete>
  44 + </div>
  45 + <tb-json-content
  46 + #payloadContent
  47 + fxFlex
  48 + formControlName="msg"
  49 + label="{{ 'rulenode.message' | translate }}"
  50 + [contentType]="contentTypes.JSON"
  51 + validateContent
  52 + [fillHeight]="true">
  53 + </tb-json-content>
  54 + </div>
41 </div> 55 </div>
42 </div> 56 </div>
43 <div #topRightPanel class="tb-split tb-content"> 57 <div #topRightPanel class="tb-split tb-content">
@@ -45,7 +59,10 @@ @@ -45,7 +59,10 @@
45 <div class="tb-editor-area-title-panel"> 59 <div class="tb-editor-area-title-panel">
46 <label translate>rulenode.metadata</label> 60 <label translate>rulenode.metadata</label>
47 </div> 61 </div>
48 - TODO: metadataForm 62 + <tb-key-val-map
  63 + formControlName="metadata"
  64 + titleText="rulenode.metadata">
  65 + </tb-key-val-map>
49 </div> 66 </div>
50 </div> 67 </div>
51 </div> 68 </div>
@@ -55,7 +72,14 @@ @@ -55,7 +72,14 @@
55 <div class="tb-editor-area-title-panel tb-js-function"> 72 <div class="tb-editor-area-title-panel tb-js-function">
56 <label>{{ functionTitle }}</label> 73 <label>{{ functionTitle }}</label>
57 </div> 74 </div>
58 - TODO: funcBodyForm 75 + <tb-js-func
  76 + formControlName="script"
  77 + functionName="{{ data.functionName }}"
  78 + [functionArgs]="data.argNames"
  79 + [validationArgs]="[data.msg, data.metadata, data.msgType]"
  80 + resultType="object"
  81 + [fillHeight]="true">
  82 + </tb-js-func>
59 </div> 83 </div>
60 </div> 84 </div>
61 <div #bottomRightPanel class="tb-split tb-content"> 85 <div #bottomRightPanel class="tb-split tb-content">
@@ -63,7 +87,15 @@ @@ -63,7 +87,15 @@
63 <div class="tb-editor-area-title-panel"> 87 <div class="tb-editor-area-title-panel">
64 <label translate>rulenode.output</label> 88 <label translate>rulenode.output</label>
65 </div> 89 </div>
66 - TODO: output 90 + <tb-json-content
  91 + fxFlex
  92 + formControlName="output"
  93 + label="{{ 'rulenode.output' | translate }}"
  94 + [contentType]="contentTypes.JSON"
  95 + validateContent="false"
  96 + readonly="true"
  97 + [fillHeight]="true">
  98 + </tb-json-content>
67 </div> 99 </div>
68 </div> 100 </div>
69 </div> 101 </div>
@@ -79,7 +111,7 @@ @@ -79,7 +111,7 @@
79 <span fxFlex></span> 111 <span fxFlex></span>
80 <button mat-button mat-raised-button color="primary" 112 <button mat-button mat-raised-button color="primary"
81 type="submit" 113 type="submit"
82 - [disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('funcBody').invalid || !nodeScriptTestFormGroup.get('funcBody').dirty"> 114 + [disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('script').invalid || !nodeScriptTestFormGroup.get('script').dirty">
83 {{ 'action.save' | translate }} 115 {{ 'action.save' | translate }}
84 </button> 116 </button>
85 <button mat-button color="primary" 117 <button mat-button color="primary"
@@ -23,6 +23,10 @@ @@ -23,6 +23,10 @@
23 overflow-y: auto; 23 overflow-y: auto;
24 } 24 }
25 25
  26 + .ace_editor {
  27 + font-size: 14px !important;
  28 + }
  29 +
26 .tb-content { 30 .tb-content {
27 padding-top: 5px; 31 padding-top: 5px;
28 padding-left: 5px; 32 padding-left: 5px;
@@ -21,7 +21,7 @@ import { @@ -21,7 +21,7 @@ import {
21 Inject, 21 Inject,
22 OnInit, 22 OnInit,
23 QueryList, 23 QueryList,
24 - SkipSelf, 24 + SkipSelf, ViewChild,
25 ViewChildren, 25 ViewChildren,
26 ViewEncapsulation 26 ViewEncapsulation
27 } from '@angular/core'; 27 } from '@angular/core';
@@ -29,9 +29,16 @@ import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/mater @@ -29,9 +29,16 @@ import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/mater
29 import { Store } from '@ngrx/store'; 29 import { Store } from '@ngrx/store';
30 import { AppState } from '@core/core.state'; 30 import { AppState } from '@core/core.state';
31 import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; 31 import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
32 -import { combineLatest } from 'rxjs'; 32 +import { combineLatest, never, Observable, of, throwError, NEVER } from 'rxjs';
33 import { Router } from '@angular/router'; 33 import { Router } from '@angular/router';
34 import { DialogComponent } from '@app/shared/components/dialog.component'; 34 import { DialogComponent } from '@app/shared/components/dialog.component';
  35 +import { ContentType } from '@shared/models/constants';
  36 +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
  37 +import { JsonContentComponent } from '@shared/components/json-content.component';
  38 +import { TestScriptInputParams } from '@shared/models/rule-node.models';
  39 +import { RuleChainService } from '@core/http/rule-chain.service';
  40 +import { map, mergeMap } from 'rxjs/operators';
  41 +import { ActionNotificationShow } from '@core/notification/notification.actions';
35 42
36 export interface NodeScriptTestDialogData { 43 export interface NodeScriptTestDialogData {
37 script: string; 44 script: string;
@@ -40,7 +47,7 @@ export interface NodeScriptTestDialogData { @@ -40,7 +47,7 @@ export interface NodeScriptTestDialogData {
40 functionName: string; 47 functionName: string;
41 argNames: string[]; 48 argNames: string[];
42 msg?: any; 49 msg?: any;
43 - metadata?: any; 50 + metadata?: {[key: string]: string};
44 msgType?: string; 51 msgType?: string;
45 } 52 }
46 53
@@ -75,46 +82,46 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes @@ -75,46 +82,46 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
75 @ViewChildren('bottomRightPanel') 82 @ViewChildren('bottomRightPanel')
76 bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>; 83 bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>;
77 84
  85 + @ViewChild('payloadContent', {static: true}) payloadContent: JsonContentComponent;
  86 +
78 nodeScriptTestFormGroup: FormGroup; 87 nodeScriptTestFormGroup: FormGroup;
79 88
80 functionTitle: string; 89 functionTitle: string;
81 90
82 submitted = false; 91 submitted = false;
83 92
  93 + contentTypes = ContentType;
  94 +
84 constructor(protected store: Store<AppState>, 95 constructor(protected store: Store<AppState>,
85 protected router: Router, 96 protected router: Router,
86 @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData, 97 @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData,
87 @SkipSelf() private errorStateMatcher: ErrorStateMatcher, 98 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
88 public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>, 99 public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>,
89 - public fb: FormBuilder) { 100 + public fb: FormBuilder,
  101 + private ruleChainService: RuleChainService) {
90 super(store, router, dialogRef); 102 super(store, router, dialogRef);
91 this.functionTitle = this.data.functionTitle; 103 this.functionTitle = this.data.functionTitle;
92 } 104 }
93 105
94 ngOnInit(): void { 106 ngOnInit(): void {
95 this.nodeScriptTestFormGroup = this.fb.group({ 107 this.nodeScriptTestFormGroup = this.fb.group({
96 - funcBody: ['', [Validators.required]] 108 + payload: this.fb.group({
  109 + msgType: [this.data.msgType, [Validators.required]],
  110 + msg: [js_beautify(JSON.stringify(this.data.msg), {indent_size: 4}), []],
  111 + }),
  112 + metadata: [this.data.metadata, [Validators.required]],
  113 + script: [this.data.script, []],
  114 + output: ['', []]
97 }); 115 });
98 } 116 }
99 117
100 ngAfterViewInit(): void { 118 ngAfterViewInit(): void {
101 -/* combineLatest(this.topPanelElmRef.changes,  
102 - this.topLeftPanelElmRef.changes,  
103 - this.topRightPanelElmRef.changes,  
104 - this.bottomPanelElmRef.changes,  
105 - this.bottomLeftPanelElmRef.changes,  
106 - this.bottomRightPanelElmRef.changes).subscribe(() => {  
107 - if (this.topPanelElmRef.length && this.topLeftPanelElmRef.length &&  
108 - this.topRightPanelElmRef.length && this.bottomPanelElmRef.length &&  
109 - this.bottomLeftPanelElmRef.length && this.bottomRightPanelElmRef.length) {*/  
110 - this.initSplitLayout(this.topPanelElmRef.first.nativeElement,  
111 - this.topLeftPanelElmRef.first.nativeElement,  
112 - this.topRightPanelElmRef.first.nativeElement,  
113 - this.bottomPanelElmRef.first.nativeElement,  
114 - this.bottomLeftPanelElmRef.first.nativeElement,  
115 - this.bottomRightPanelElmRef.first.nativeElement);  
116 - // }  
117 - //}); 119 + this.initSplitLayout(this.topPanelElmRef.first.nativeElement,
  120 + this.topLeftPanelElmRef.first.nativeElement,
  121 + this.topRightPanelElmRef.first.nativeElement,
  122 + this.bottomPanelElmRef.first.nativeElement,
  123 + this.bottomLeftPanelElmRef.first.nativeElement,
  124 + this.bottomRightPanelElmRef.first.nativeElement);
118 } 125 }
119 126
120 private initSplitLayout(topPanel: any, 127 private initSplitLayout(topPanel: any,
@@ -154,9 +161,54 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes @@ -154,9 +161,54 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
154 this.dialogRef.close(null); 161 this.dialogRef.close(null);
155 } 162 }
156 163
  164 + test(): void {
  165 + this.testNodeScript().subscribe((output) => {
  166 + this.nodeScriptTestFormGroup.get('output').setValue(js_beautify(output, {indent_size: 4}));
  167 + });
  168 + }
  169 +
  170 + private testNodeScript(): Observable<string> {
  171 + if (this.checkInputParamErrors()) {
  172 + const inputParams: TestScriptInputParams = {
  173 + argNames: this.data.argNames,
  174 + scriptType: this.data.scriptType,
  175 + msgType: this.nodeScriptTestFormGroup.get('payload').get('msgType').value,
  176 + msg: this.nodeScriptTestFormGroup.get('payload').get('msg').value,
  177 + metadata: this.nodeScriptTestFormGroup.get('metadata').value,
  178 + script: this.nodeScriptTestFormGroup.get('script').value
  179 + };
  180 + return this.ruleChainService.testScript(inputParams).pipe(
  181 + mergeMap((result) => {
  182 + if (result.error) {
  183 + this.store.dispatch(new ActionNotificationShow(
  184 + {
  185 + message: result.error,
  186 + type: 'error'
  187 + }));
  188 + return NEVER;
  189 + } else {
  190 + return of(result.output);
  191 + }
  192 + })
  193 + );
  194 + } else {
  195 + return NEVER;
  196 + }
  197 + }
  198 +
  199 + private checkInputParamErrors(): boolean {
  200 + this.payloadContent.validateOnSubmit();
  201 + if (!this.nodeScriptTestFormGroup.get('payload').valid) {
  202 + return false;
  203 + }
  204 + return true;
  205 + }
  206 +
157 save(): void { 207 save(): void {
158 this.submitted = true; 208 this.submitted = true;
159 - const script: string = this.nodeScriptTestFormGroup.get('funcBody').value;  
160 - this.dialogRef.close(script); 209 + this.testNodeScript().subscribe(() => {
  210 + this.nodeScriptTestFormGroup.get('script').markAsPristine();
  211 + this.dialogRef.close(this.nodeScriptTestFormGroup.get('script').value);
  212 + });
161 } 213 }
162 } 214 }
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
  19 + tb-fullscreen
  20 + [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
  21 + <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar">
  22 + <label class="tb-title no-padding">{{ label }}</label>
  23 + <span fxFlex></span>
  24 + <button type="button"
  25 + mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()">
  26 + {{'js-func.tidy' | translate }}
  27 + </button>
  28 + <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
  29 + class="tb-mat-32"
  30 + matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  31 + matTooltipPosition="above">
  32 + <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  33 + </button>
  34 + </div>
  35 + <div id="tb-json-panel" tb-toast toastTarget="jsonContentEditor"
  36 + class="tb-json-content-panel" fxLayout="column">
  37 + <div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
  38 + </div>
  39 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + position: relative;
  19 +
  20 + .fill-height {
  21 + height: 100%;
  22 + }
  23 +}
  24 +
  25 +.tb-json-content-toolbar {
  26 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  27 + align-items: center;
  28 + vertical-align: middle;
  29 + min-width: 32px;
  30 + min-height: 15px;
  31 + padding: 4px;
  32 + margin: 0;
  33 + font-size: .8rem;
  34 + line-height: 15px;
  35 + color: #7b7b7b;
  36 + background: rgba(220, 220, 220, .35);
  37 + &:not(:last-child) {
  38 + margin-right: 4px;
  39 + }
  40 + }
  41 +}
  42 +
  43 +.tb-json-content-panel {
  44 + height: 100%;
  45 + margin-left: 15px;
  46 + border: 1px solid #c0c0c0;
  47 +
  48 + #tb-json-input {
  49 + width: 100%;
  50 + min-width: 200px;
  51 + height: 100%;
  52 +
  53 + &:not(.fill-height) {
  54 + min-height: 200px;
  55 + }
  56 + }
  57 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + ElementRef,
  20 + forwardRef,
  21 + Input,
  22 + OnChanges,
  23 + OnInit,
  24 + ViewChild,
  25 + SimpleChanges,
  26 + OnDestroy
  27 +} from '@angular/core';
  28 +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
  29 +import * as ace from 'ace-builds';
  30 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  31 +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
  32 +import { Store } from '@ngrx/store';
  33 +import { AppState } from '@core/core.state';
  34 +import { ContentType, contentTypesMap } from '@shared/models/constants';
  35 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
  36 +
  37 +@Component({
  38 + selector: 'tb-json-content',
  39 + templateUrl: './json-content.component.html',
  40 + styleUrls: ['./json-content.component.scss'],
  41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => JsonContentComponent),
  45 + multi: true
  46 + },
  47 + {
  48 + provide: NG_VALIDATORS,
  49 + useExisting: forwardRef(() => JsonContentComponent),
  50 + multi: true,
  51 + }
  52 + ]
  53 +})
  54 +export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy {
  55 +
  56 + @ViewChild('jsonEditor', {static: true})
  57 + jsonEditorElmRef: ElementRef;
  58 +
  59 + private jsonEditor: ace.Ace.Editor;
  60 + private editorsResizeCaf: CancelAnimationFrame;
  61 + private editorResizeListener: any;
  62 +
  63 + @Input() label: string;
  64 +
  65 + @Input() contentType: ContentType;
  66 +
  67 + @Input() disabled: boolean;
  68 +
  69 + @Input() fillHeight: boolean;
  70 +
  71 + @Input() editorStyle: {[klass: string]: any};
  72 +
  73 + private readonlyValue: boolean;
  74 + get readonly(): boolean {
  75 + return this.readonlyValue;
  76 + }
  77 + @Input()
  78 + set readonly(value: boolean) {
  79 + this.readonlyValue = coerceBooleanProperty(value);
  80 + }
  81 +
  82 + private validateContentValue: boolean;
  83 + get validateContent(): boolean {
  84 + return this.validateContentValue;
  85 + }
  86 + @Input()
  87 + set validateContent(value: boolean) {
  88 + this.validateContentValue = coerceBooleanProperty(value);
  89 + }
  90 +
  91 + fullscreen = false;
  92 +
  93 + contentBody: string;
  94 +
  95 + contentValid: boolean;
  96 +
  97 + validationError: string;
  98 +
  99 + errorShowed = false;
  100 +
  101 + private propagateChange = null;
  102 +
  103 + constructor(public elementRef: ElementRef,
  104 + protected store: Store<AppState>,
  105 + private raf: RafService) {
  106 + }
  107 +
  108 + ngOnInit(): void {
  109 + const editorElement = this.jsonEditorElmRef.nativeElement;
  110 + let mode = 'text';
  111 + if (this.contentType) {
  112 + mode = contentTypesMap.get(this.contentType).code;
  113 + }
  114 + let editorOptions: Partial<ace.Ace.EditorOptions> = {
  115 + mode: `ace/mode/${mode}`,
  116 + theme: 'ace/theme/github',
  117 + showGutter: true,
  118 + showPrintMargin: false,
  119 + readOnly: this.readonly
  120 + };
  121 +
  122 + const advancedOptions = {
  123 + enableSnippets: true,
  124 + enableBasicAutocompletion: true,
  125 + enableLiveAutocompletion: true
  126 + };
  127 +
  128 + editorOptions = {...editorOptions, ...advancedOptions};
  129 + this.jsonEditor = ace.edit(editorElement, editorOptions);
  130 + this.jsonEditor.session.setUseWrapMode(true);
  131 + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  132 + this.jsonEditor.on('change', () => {
  133 + this.cleanupJsonErrors();
  134 + this.updateView();
  135 + });
  136 + this.editorResizeListener = this.onAceEditorResize.bind(this);
  137 + // @ts-ignore
  138 + addResizeListener(editorElement, this.editorResizeListener);
  139 + }
  140 +
  141 + ngOnDestroy(): void {
  142 + if (this.editorResizeListener) {
  143 + const editorElement = this.jsonEditorElmRef.nativeElement;
  144 + // @ts-ignore
  145 + removeResizeListener(editorElement, this.editorResizeListener);
  146 + }
  147 + }
  148 +
  149 + private onAceEditorResize() {
  150 + if (this.editorsResizeCaf) {
  151 + this.editorsResizeCaf();
  152 + this.editorsResizeCaf = null;
  153 + }
  154 + this.editorsResizeCaf = this.raf.raf(() => {
  155 + this.jsonEditor.resize();
  156 + this.jsonEditor.renderer.updateFull();
  157 + });
  158 + }
  159 +
  160 + ngOnChanges(changes: SimpleChanges): void {
  161 + for (const propName of Object.keys(changes)) {
  162 + const change = changes[propName];
  163 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  164 + if (propName === 'contentType') {
  165 + if (this.jsonEditor) {
  166 + let mode = 'text';
  167 + if (this.contentType) {
  168 + mode = contentTypesMap.get(this.contentType).code;
  169 + }
  170 + this.jsonEditor.session.setMode(`ace/mode/${mode}`);
  171 + }
  172 + }
  173 + }
  174 + }
  175 + }
  176 +
  177 + registerOnChange(fn: any): void {
  178 + this.propagateChange = fn;
  179 + }
  180 +
  181 + registerOnTouched(fn: any): void {
  182 + }
  183 +
  184 + setDisabledState(isDisabled: boolean): void {
  185 + this.disabled = isDisabled;
  186 + }
  187 +
  188 + public validate(c: FormControl) {
  189 + return (this.contentValid) ? null : {
  190 + contentBody: {
  191 + valid: false,
  192 + },
  193 + };
  194 + }
  195 +
  196 + validateOnSubmit(): void {
  197 + if (!this.readonly) {
  198 + this.cleanupJsonErrors();
  199 + this.contentValid = true;
  200 + this.propagateChange(this.contentBody);
  201 + this.contentValid = this.doValidate();
  202 + this.propagateChange(this.contentBody);
  203 + }
  204 + }
  205 +
  206 + private doValidate(): boolean {
  207 + try {
  208 + if (this.validateContent && this.contentType === ContentType.JSON) {
  209 + JSON.parse(this.contentBody);
  210 + }
  211 + return true;
  212 + } catch (ex) {
  213 + let errorInfo = 'Error:';
  214 + if (ex.name) {
  215 + errorInfo += ' ' + ex.name + ':';
  216 + }
  217 + if (ex.message) {
  218 + errorInfo += ' ' + ex.message;
  219 + }
  220 + this.store.dispatch(new ActionNotificationShow(
  221 + {
  222 + message: errorInfo,
  223 + type: 'error',
  224 + target: 'jsonContentEditor',
  225 + verticalPosition: 'bottom',
  226 + horizontalPosition: 'left'
  227 + }));
  228 + this.errorShowed = true;
  229 + return false;
  230 + }
  231 + }
  232 +
  233 + cleanupJsonErrors(): void {
  234 + if (this.errorShowed) {
  235 + this.store.dispatch(new ActionNotificationHide(
  236 + {
  237 + target: 'jsonContentEditor'
  238 + }));
  239 + this.errorShowed = false;
  240 + }
  241 + }
  242 +
  243 + writeValue(value: string): void {
  244 + this.contentBody = value;
  245 + this.contentValid = true;
  246 + if (this.jsonEditor) {
  247 + this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  248 + // this.jsonEditor.
  249 + }
  250 + }
  251 +
  252 + updateView() {
  253 + const editorValue = this.jsonEditor.getValue();
  254 + if (this.contentBody !== editorValue) {
  255 + this.contentBody = editorValue;
  256 + this.contentValid = true;
  257 + this.propagateChange(this.contentBody);
  258 + }
  259 + }
  260 +
  261 + beautifyJson() {
  262 + const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60});
  263 + this.jsonEditor.setValue(res ? res : '', -1);
  264 + this.updateView();
  265 + }
  266 +
  267 + onFullscreen() {
  268 + if (this.jsonEditor) {
  269 + setTimeout(() => {
  270 + this.jsonEditor.resize();
  271 + }, 0);
  272 + }
  273 + }
  274 +
  275 +}
@@ -19,7 +19,7 @@ import { @@ -19,7 +19,7 @@ import {
19 Component, 19 Component,
20 ElementRef, 20 ElementRef,
21 forwardRef, 21 forwardRef,
22 - Input, 22 + Input, OnDestroy,
23 OnInit, 23 OnInit,
24 ViewChild 24 ViewChild
25 } from '@angular/core'; 25 } from '@angular/core';
@@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
29 import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; 29 import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
30 import { Store } from '@ngrx/store'; 30 import { Store } from '@ngrx/store';
31 import { AppState } from '@core/core.state'; 31 import { AppState } from '@core/core.state';
  32 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
32 33
33 @Component({ 34 @Component({
34 selector: 'tb-json-object-edit', 35 selector: 'tb-json-object-edit',
@@ -47,12 +48,14 @@ import { AppState } from '@core/core.state'; @@ -47,12 +48,14 @@ import { AppState } from '@core/core.state';
47 } 48 }
48 ] 49 ]
49 }) 50 })
50 -export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator { 51 +export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy {
51 52
52 @ViewChild('jsonEditor', {static: true}) 53 @ViewChild('jsonEditor', {static: true})
53 jsonEditorElmRef: ElementRef; 54 jsonEditorElmRef: ElementRef;
54 55
55 private jsonEditor: ace.Ace.Editor; 56 private jsonEditor: ace.Ace.Editor;
  57 + private editorsResizeCaf: CancelAnimationFrame;
  58 + private editorResizeListener: any;
56 59
57 @Input() label: string; 60 @Input() label: string;
58 61
@@ -95,7 +98,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @@ -95,7 +98,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
95 private propagateChange = null; 98 private propagateChange = null;
96 99
97 constructor(public elementRef: ElementRef, 100 constructor(public elementRef: ElementRef,
98 - protected store: Store<AppState>) { 101 + protected store: Store<AppState>,
  102 + private raf: RafService) {
99 } 103 }
100 104
101 ngOnInit(): void { 105 ngOnInit(): void {
@@ -122,6 +126,28 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @@ -122,6 +126,28 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
122 this.cleanupJsonErrors(); 126 this.cleanupJsonErrors();
123 this.updateView(); 127 this.updateView();
124 }); 128 });
  129 + this.editorResizeListener = this.onAceEditorResize.bind(this);
  130 + // @ts-ignore
  131 + addResizeListener(editorElement, this.editorResizeListener);
  132 + }
  133 +
  134 + ngOnDestroy(): void {
  135 + if (this.editorResizeListener) {
  136 + const editorElement = this.jsonEditorElmRef.nativeElement;
  137 + // @ts-ignore
  138 + removeResizeListener(editorElement, this.editorResizeListener);
  139 + }
  140 + }
  141 +
  142 + private onAceEditorResize() {
  143 + if (this.editorsResizeCaf) {
  144 + this.editorsResizeCaf();
  145 + this.editorsResizeCaf = null;
  146 + }
  147 + this.editorsResizeCaf = this.raf.raf(() => {
  148 + this.jsonEditor.resize();
  149 + this.jsonEditor.renderer.updateFull();
  150 + });
125 } 151 }
126 152
127 registerOnChange(fn: any): void { 153 registerOnChange(fn: any): void {
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<section fxLayout="column" class="tb-kv-map" [formGroup]="kvListFormGroup">
  20 + <label translate class="tb-title no-padding">{{ titleText }}</label>
  21 + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;"
  22 + formArrayName="keyVals"
  23 + *ngFor="let keyValControl of keyValsFormArray().controls; let $index = index">
  24 + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block"
  25 + style="max-height: 40px;">
  26 + <mat-label></mat-label>
  27 + <input [formControl]="keyValControl.get('key')" matInput required
  28 + placeholder="{{ (keyPlaceholderText ? keyPlaceholderText : 'key-val.key') | translate }}"/>
  29 + </mat-form-field>
  30 + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block"
  31 + style="max-height: 40px;">
  32 + <mat-label></mat-label>
  33 + <input [formControl]="keyValControl.get('value')" matInput required
  34 + placeholder="{{ (valuePlaceholderText ? valuePlaceholderText : 'key-val.value') | translate }}"/>
  35 + </mat-form-field>
  36 + <button mat-button mat-icon-button color="primary"
  37 + [fxShow]="!disabled"
  38 + type="button"
  39 + (click)="removeKeyVal($index)"
  40 + [disabled]="isLoading$ | async"
  41 + matTooltip="{{ 'key-val.remove-entry' | translate }}"
  42 + matTooltipPosition="above">
  43 + <mat-icon>close</mat-icon>
  44 + </button>
  45 + </div>
  46 + <span [fxShow]="!keyValsFormArray().length"
  47 + fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
  48 + class="no-data-found" translate>{{noDataText ? noDataText : 'key-val.no-data'}}</span>
  49 + <div style="margin-top: 8px;">
  50 + <button mat-button mat-raised-button color="primary"
  51 + [fxShow]="!disabled"
  52 + [disabled]="isLoading$ | async"
  53 + (click)="addKeyVal()"
  54 + type="button"
  55 + matTooltip="{{ 'key-val.add-entry' | translate }}"
  56 + matTooltipPosition="above">
  57 + {{ 'action.add' | translate }}
  58 + </button>
  59 + </div>
  60 +</section>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-kv-map {
  18 + span.no-data-found {
  19 + position: relative;
  20 + display: flex;
  21 + height: 40px;
  22 + text-transform: uppercase;
  23 +
  24 + &.disabled {
  25 + color: rgba(0, 0, 0, .38);
  26 + }
  27 + }
  28 + }
  29 +}
  30 +
  31 +:host ::ng-deep {
  32 + .mat-form-field-wrapper {
  33 + padding-bottom: 0;
  34 + }
  35 + .mat-form-field-infix {
  36 + border-top: 0;
  37 + }
  38 + .mat-form-field-underline {
  39 + bottom: 0;
  40 + }
  41 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
  18 +import {
  19 + AbstractControl,
  20 + ControlValueAccessor, FormArray,
  21 + FormBuilder, FormControl,
  22 + FormGroup, NG_VALIDATORS,
  23 + NG_VALUE_ACCESSOR, Validator,
  24 + ValidatorFn,
  25 + Validators
  26 +} from '@angular/forms';
  27 +import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
  28 +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
  29 +import { TranslateService } from '@ngx-translate/core';
  30 +import { EntityService } from '@core/http/entity.service';
  31 +import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models';
  32 +import { PageComponent } from '@shared/components/page.component';
  33 +import { Store } from '@ngrx/store';
  34 +import { AppState } from '@core/core.state';
  35 +import { Subscription } from 'rxjs';
  36 +
  37 +@Component({
  38 + selector: 'tb-key-val-map',
  39 + templateUrl: './kv-map.component.html',
  40 + styleUrls: ['./kv-map.component.scss'],
  41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => KeyValMapComponent),
  45 + multi: true
  46 + },
  47 + {
  48 + provide: NG_VALIDATORS,
  49 + useExisting: forwardRef(() => KeyValMapComponent),
  50 + multi: true,
  51 + }
  52 + ]
  53 +})
  54 +export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator {
  55 +
  56 + @Input() disabled: boolean;
  57 +
  58 + @Input() titleText: string;
  59 +
  60 + @Input() keyPlaceholderText: string;
  61 +
  62 + @Input() valuePlaceholderText: string;
  63 +
  64 + @Input() noDataText: string;
  65 +
  66 + kvListFormGroup: FormGroup;
  67 +
  68 + private propagateChange = null;
  69 +
  70 + private valueChangeSubscription: Subscription = null;
  71 +
  72 + constructor(protected store: Store<AppState>,
  73 + private fb: FormBuilder) {
  74 + super(store);
  75 + }
  76 +
  77 + ngOnInit(): void {
  78 + this.kvListFormGroup = this.fb.group({});
  79 + this.kvListFormGroup.addControl('keyVals',
  80 + this.fb.array([]));
  81 + }
  82 +
  83 + keyValsFormArray(): FormArray {
  84 + return this.kvListFormGroup.get('keyVals') as FormArray;
  85 + }
  86 +
  87 + registerOnChange(fn: any): void {
  88 + this.propagateChange = fn;
  89 + }
  90 +
  91 + registerOnTouched(fn: any): void {
  92 + }
  93 +
  94 + setDisabledState?(isDisabled: boolean): void {
  95 + this.disabled = isDisabled;
  96 + if (this.disabled) {
  97 + this.kvListFormGroup.disable({emitEvent: false});
  98 + } else {
  99 + this.kvListFormGroup.enable({emitEvent: false});
  100 + }
  101 + }
  102 +
  103 + writeValue(keyValMap: {[key: string]: string}): void {
  104 + if (this.valueChangeSubscription) {
  105 + this.valueChangeSubscription.unsubscribe();
  106 + }
  107 + const keyValsControls: Array<AbstractControl> = [];
  108 + if (keyValMap) {
  109 + for (const property of Object.keys(keyValMap)) {
  110 + if (Object.prototype.hasOwnProperty.call(keyValMap, property)) {
  111 + keyValsControls.push(this.fb.group({
  112 + key: [property, [Validators.required]],
  113 + value: [keyValMap[property], [Validators.required]]
  114 + }));
  115 + }
  116 + }
  117 + }
  118 + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls));
  119 + this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => {
  120 + this.updateModel();
  121 + });
  122 + }
  123 +
  124 + public removeKeyVal(index: number) {
  125 + (this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index);
  126 + }
  127 +
  128 + public addKeyVal() {
  129 + const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray;
  130 + keyValsFormArray.push(this.fb.group({
  131 + key: ['', [Validators.required]],
  132 + value: ['', [Validators.required]]
  133 + }));
  134 + }
  135 +
  136 + public validate(c: FormControl) {
  137 + const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value;
  138 + let valid = true;
  139 + for (const entry of kvList) {
  140 + if (!entry.key || !entry.value) {
  141 + valid = false;
  142 + break;
  143 + }
  144 + }
  145 + return (valid) ? null : {
  146 + keyVals: {
  147 + valid: false,
  148 + },
  149 + };
  150 + }
  151 +
  152 + private updateModel() {
  153 + const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value;
  154 + const keyValMap: {[key: string]: string} = {};
  155 + kvList.forEach((entry) => {
  156 + keyValMap[entry.key] = entry.value;
  157 + });
  158 + this.propagateChange(keyValMap);
  159 + }
  160 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-form-field [formGroup]="messageTypeFormGroup" class="mat-block">
  19 + <mat-label>{{ 'rulenode.message-type' | translate }}</mat-label>
  20 + <input matInput type="text" placeholder="{{ 'rulenode.select-message-type' | translate }}"
  21 + #messageTypeInput
  22 + formControlName="messageType"
  23 + (focusin)="onFocus()"
  24 + [required]="required"
  25 + [matAutocomplete]="messageTypeAutocomplete">
  26 + <button *ngIf="messageTypeFormGroup.get('messageType').value && !disabled"
  27 + type="button"
  28 + matSuffix mat-button mat-icon-button aria-label="Clear"
  29 + (click)="clear()">
  30 + <mat-icon class="material-icons">close</mat-icon>
  31 + </button>
  32 + <mat-autocomplete
  33 + class="tb-autocomplete"
  34 + #messageTypeAutocomplete="matAutocomplete"
  35 + [displayWith]="displayMessageTypeFn">
  36 + <mat-option *ngFor="let messageType of filteredMessageTypes | async" [value]="messageType">
  37 + <span [innerHTML]="displayMessageTypeFn(messageType) | highlight:searchText"></span>
  38 + </mat-option>
  39 + </mat-autocomplete>
  40 + <mat-error *ngIf="messageTypeFormGroup.get('messageType').hasError('required')">
  41 + {{ 'rulenode.message-type-required' | translate }}
  42 + </mat-error>
  43 +</mat-form-field>
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { Observable, of } from 'rxjs';
  20 +import { map, mergeMap, startWith, tap } from 'rxjs/operators';
  21 +import { Store } from '@ngrx/store';
  22 +import { AppState } from '@app/core/core.state';
  23 +import { TranslateService } from '@ngx-translate/core';
  24 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  25 +import { MessageType, messageTypeNames } from '@shared/models/rule-node.models';
  26 +
  27 +@Component({
  28 + selector: 'tb-message-type-autocomplete',
  29 + templateUrl: './message-type-autocomplete.component.html',
  30 + styleUrls: [],
  31 + providers: [{
  32 + provide: NG_VALUE_ACCESSOR,
  33 + useExisting: forwardRef(() => MessageTypeAutocompleteComponent),
  34 + multi: true
  35 + }]
  36 +})
  37 +export class MessageTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
  38 +
  39 + messageTypeFormGroup: FormGroup;
  40 +
  41 + modelValue: string | null;
  42 +
  43 + private requiredValue: boolean;
  44 + get required(): boolean {
  45 + return this.requiredValue;
  46 + }
  47 + @Input()
  48 + set required(value: boolean) {
  49 + this.requiredValue = coerceBooleanProperty(value);
  50 + }
  51 +
  52 + @Input()
  53 + disabled: boolean;
  54 +
  55 + @ViewChild('messageTypeInput', {static: true}) messageTypeInput: ElementRef;
  56 +
  57 + filteredMessageTypes: Observable<Array<MessageType | string>>;
  58 +
  59 + searchText = '';
  60 +
  61 + private dirty = false;
  62 +
  63 + private propagateChange = (v: any) => { };
  64 +
  65 + constructor(private store: Store<AppState>,
  66 + public translate: TranslateService,
  67 + private fb: FormBuilder) {
  68 + this.messageTypeFormGroup = this.fb.group({
  69 + messageType: [null]
  70 + });
  71 + }
  72 +
  73 + registerOnChange(fn: any): void {
  74 + this.propagateChange = fn;
  75 + }
  76 +
  77 + registerOnTouched(fn: any): void {
  78 + }
  79 +
  80 + ngOnInit() {
  81 + this.filteredMessageTypes = this.messageTypeFormGroup.get('messageType').valueChanges
  82 + .pipe(
  83 + tap(value => {
  84 + this.updateView(value);
  85 + }),
  86 + startWith<string | MessageType>(''),
  87 + map(value => value ? value : ''),
  88 + mergeMap(messageType => this.fetchMessageTypes(messageType) )
  89 + );
  90 + }
  91 +
  92 + ngAfterViewInit(): void {
  93 + }
  94 +
  95 + ngOnDestroy(): void {
  96 + }
  97 +
  98 + setDisabledState(isDisabled: boolean): void {
  99 + this.disabled = isDisabled;
  100 + if (this.disabled) {
  101 + this.messageTypeFormGroup.disable({emitEvent: false});
  102 + } else {
  103 + this.messageTypeFormGroup.enable({emitEvent: false});
  104 + }
  105 + }
  106 +
  107 + writeValue(value: string | null): void {
  108 + this.searchText = '';
  109 + this.modelValue = value;
  110 + let res: MessageType | string = null;
  111 + if (value) {
  112 + if (Object.values(MessageType).includes(value)) {
  113 + res = MessageType[value];
  114 + } else {
  115 + res = value;
  116 + }
  117 + }
  118 + this.messageTypeFormGroup.get('messageType').patchValue(res, {emitEvent: false});
  119 + this.dirty = true;
  120 + }
  121 +
  122 + onFocus() {
  123 + if (this.dirty) {
  124 + this.messageTypeFormGroup.get('messageType').updateValueAndValidity({onlySelf: true, emitEvent: true});
  125 + this.dirty = false;
  126 + }
  127 + }
  128 +
  129 + updateView(value: MessageType | string | null) {
  130 + let res: string = null;
  131 + if (value) {
  132 + if (Object.values(MessageType).includes(value)) {
  133 + res = MessageType[value];
  134 + } else {
  135 + res = value;
  136 + }
  137 + }
  138 + if (this.modelValue !== res) {
  139 + this.modelValue = res;
  140 + this.propagateChange(this.modelValue);
  141 + }
  142 + }
  143 +
  144 + displayMessageTypeFn(messageType?: MessageType | string): string | undefined {
  145 + if (messageType) {
  146 + if (Object.values(MessageType).includes(messageType)) {
  147 + return messageTypeNames.get(MessageType[messageType]);
  148 + } else {
  149 + return messageType;
  150 + }
  151 + }
  152 + return undefined;
  153 + }
  154 +
  155 + fetchMessageTypes(searchText?: string): Observable<Array<MessageType | string>> {
  156 + this.searchText = searchText;
  157 + const result: Array<MessageType | string> = [];
  158 + messageTypeNames.forEach((value, key) => {
  159 + if (value.toUpperCase().includes(searchText.toUpperCase())) {
  160 + result.push(key);
  161 + }
  162 + });
  163 + if (result.length) {
  164 + return of(result);
  165 + } else {
  166 + return of([searchText]);
  167 + }
  168 + }
  169 +
  170 + clear() {
  171 + this.messageTypeFormGroup.get('messageType').patchValue(null, {emitEvent: true});
  172 + setTimeout(() => {
  173 + this.messageTypeInput.nativeElement.blur();
  174 + this.messageTypeInput.nativeElement.focus();
  175 + }, 0);
  176 + }
  177 +
  178 +}
@@ -156,4 +156,41 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>( @@ -156,4 +156,41 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
156 ] 156 ]
157 ); 157 );
158 158
  159 +export interface ContentTypeData {
  160 + name: string;
  161 + code: string;
  162 +}
  163 +
  164 +export enum ContentType {
  165 + JSON = 'JSON',
  166 + TEXT = 'TEXT',
  167 + BINARY = 'BINARY'
  168 +}
  169 +
  170 +export const contentTypesMap = new Map<ContentType, ContentTypeData>(
  171 + [
  172 + [
  173 + ContentType.JSON,
  174 + {
  175 + name: 'content-type.json',
  176 + code: 'json'
  177 + }
  178 + ],
  179 + [
  180 + ContentType.TEXT,
  181 + {
  182 + name: 'content-type.text',
  183 + code: 'text'
  184 + }
  185 + ],
  186 + [
  187 + ContentType.BINARY,
  188 + {
  189 + name: 'content-type.binary',
  190 + code: 'text'
  191 + }
  192 + ]
  193 + ]
  194 +);
  195 +
159 export const customTranslationsPrefix = 'custom.'; 196 export const customTranslationsPrefix = 'custom.';
@@ -232,6 +232,58 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { @@ -232,6 +232,58 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
232 configurationDescriptor?: RuleNodeConfigurationDescriptor; 232 configurationDescriptor?: RuleNodeConfigurationDescriptor;
233 } 233 }
234 234
  235 +export interface TestScriptInputParams {
  236 + script: string;
  237 + scriptType: string;
  238 + argNames: string[];
  239 + msg: string;
  240 + metadata: {[key: string]: string};
  241 + msgType: string;
  242 +}
  243 +
  244 +export interface TestScriptResult {
  245 + output: string;
  246 + error: string;
  247 +}
  248 +
  249 +export enum MessageType {
  250 + POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST',
  251 + POST_TELEMETRY_REQUEST = 'POST_TELEMETRY_REQUEST',
  252 + TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST',
  253 + RPC_CALL_FROM_SERVER_TO_DEVICE = 'RPC_CALL_FROM_SERVER_TO_DEVICE',
  254 + ACTIVITY_EVENT = 'ACTIVITY_EVENT',
  255 + INACTIVITY_EVENT = 'INACTIVITY_EVENT',
  256 + CONNECT_EVENT = 'CONNECT_EVENT',
  257 + DISCONNECT_EVENT = 'DISCONNECT_EVENT',
  258 + ENTITY_CREATED = 'ENTITY_CREATED',
  259 + ENTITY_UPDATED = 'ENTITY_UPDATED',
  260 + ENTITY_DELETED = 'ENTITY_DELETED',
  261 + ENTITY_ASSIGNED = 'ENTITY_ASSIGNED',
  262 + ENTITY_UNASSIGNED = 'ENTITY_UNASSIGNED',
  263 + ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED',
  264 + ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED'
  265 +}
  266 +
  267 +export const messageTypeNames = new Map<MessageType, string>(
  268 + [
  269 + [MessageType.POST_ATTRIBUTES_REQUEST, 'Post attributes'],
  270 + [MessageType.POST_TELEMETRY_REQUEST, 'Post telemetry'],
  271 + [MessageType.TO_SERVER_RPC_REQUEST, 'RPC Request from Device'],
  272 + [MessageType.RPC_CALL_FROM_SERVER_TO_DEVICE, 'RPC Request to Device'],
  273 + [MessageType.ACTIVITY_EVENT, 'Activity Event'],
  274 + [MessageType.INACTIVITY_EVENT, 'Inactivity Event'],
  275 + [MessageType.CONNECT_EVENT, 'Connect Event'],
  276 + [MessageType.DISCONNECT_EVENT, 'Disconnect Event'],
  277 + [MessageType.ENTITY_CREATED, 'Entity Created'],
  278 + [MessageType.ENTITY_UPDATED, 'Entity Updated'],
  279 + [MessageType.ENTITY_DELETED, 'Entity Deleted'],
  280 + [MessageType.ENTITY_ASSIGNED, 'Entity Assigned'],
  281 + [MessageType.ENTITY_UNASSIGNED, 'Entity Unassigned'],
  282 + [MessageType.ATTRIBUTES_UPDATED, 'Attributes Updated'],
  283 + [MessageType.ATTRIBUTES_DELETED, 'Attributes Deleted']
  284 + ]
  285 +);
  286 +
235 const ruleNodeClazzHelpLinkMap = { 287 const ruleNodeClazzHelpLinkMap = {
236 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation', 288 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation',
237 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields', 289 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields',
@@ -115,6 +115,9 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se @@ -115,6 +115,9 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
115 import { ImageInputComponent } from './components/image-input.component'; 115 import { ImageInputComponent } from './components/image-input.component';
116 import { FileInputComponent } from './components/file-input.component'; 116 import { FileInputComponent } from './components/file-input.component';
117 import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-script-test-dialog.component'; 117 import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-script-test-dialog.component';
  118 +import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component';
  119 +import { JsonContentComponent } from './components/json-content.component';
  120 +import { KeyValMapComponent } from './components/kv-map.component';
118 121
119 @NgModule({ 122 @NgModule({
120 providers: [ 123 providers: [
@@ -175,6 +178,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc @@ -175,6 +178,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
175 RelationTypeAutocompleteComponent, 178 RelationTypeAutocompleteComponent,
176 SocialSharePanelComponent, 179 SocialSharePanelComponent,
177 JsonObjectEditComponent, 180 JsonObjectEditComponent,
  181 + JsonContentComponent,
178 JsFuncComponent, 182 JsFuncComponent,
179 FabTriggerDirective, 183 FabTriggerDirective,
180 FabActionsDirective, 184 FabActionsDirective,
@@ -188,6 +192,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc @@ -188,6 +192,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
188 JsonFormComponent, 192 JsonFormComponent,
189 ImageInputComponent, 193 ImageInputComponent,
190 FileInputComponent, 194 FileInputComponent,
  195 + MessageTypeAutocompleteComponent,
  196 + KeyValMapComponent,
191 NospacePipe, 197 NospacePipe,
192 MillisecondsToTimeStringPipe, 198 MillisecondsToTimeStringPipe,
193 EnumToArrayPipe, 199 EnumToArrayPipe,
@@ -276,6 +282,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc @@ -276,6 +282,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
276 RelationTypeAutocompleteComponent, 282 RelationTypeAutocompleteComponent,
277 SocialSharePanelComponent, 283 SocialSharePanelComponent,
278 JsonObjectEditComponent, 284 JsonObjectEditComponent,
  285 + JsonContentComponent,
279 JsFuncComponent, 286 JsFuncComponent,
280 FabTriggerDirective, 287 FabTriggerDirective,
281 FabActionsDirective, 288 FabActionsDirective,
@@ -331,6 +338,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc @@ -331,6 +338,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
331 JsonFormComponent, 338 JsonFormComponent,
332 ImageInputComponent, 339 ImageInputComponent,
333 FileInputComponent, 340 FileInputComponent,
  341 + MessageTypeAutocompleteComponent,
  342 + KeyValMapComponent,
334 NospacePipe, 343 NospacePipe,
335 MillisecondsToTimeStringPipe, 344 MillisecondsToTimeStringPipe,
336 EnumToArrayPipe, 345 EnumToArrayPipe,