Commit db02d42e3762e39f793f265ba64e081cc072a439

Authored by Igor Kulikov
1 parent 8e37e283

Rule Node Test Script

... ... @@ -31,7 +31,7 @@ import { ComponentDescriptorService } from './component-descriptor.service';
31 31 import {
32 32 IRuleNodeConfigurationComponent,
33 33 LinkLabel,
34   - RuleNodeComponentDescriptor
  34 + RuleNodeComponentDescriptor, TestScriptInputParams, TestScriptResult
35 35 } from '@app/shared/models/rule-node.models';
36 36 import { ResourcesService } from '../services/resources.service';
37 37 import { catchError, map, mergeMap } from 'rxjs/operators';
... ... @@ -175,6 +175,10 @@ export class RuleChainService {
175 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 182 private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> {
179 183 if (ruleChainConnections && ruleChainConnections.length) {
180 184 const tasks: Observable<RuleChain>[] = [];
... ...
... ... @@ -23,7 +23,7 @@ import {
23 23 HttpResponseBase
24 24 } from '@angular/common/http';
25 25 import { Observable } from 'rxjs/internal/Observable';
26   -import { Injectable } from '@angular/core';
  26 +import { Inject, Injectable } from '@angular/core';
27 27 import { AuthService } from '../auth/auth.service';
28 28 import { Constants } from '../../shared/models/constants';
29 29 import { InterceptorHttpParams } from './interceptor-http-params';
... ... @@ -52,10 +52,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
52 52
53 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 61 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
... ...
... ... @@ -36,7 +36,7 @@ export class NodeScriptTestService {
36 36 return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
37 37 switchMap((debugIn) => {
38 38 let msg: any;
39   - let metadata: any;
  39 + let metadata: {[key: string]: string};
40 40 let msgType: string;
41 41 if (debugIn) {
42 42 if (debugIn.data) {
... ... @@ -59,7 +59,7 @@ export class NodeScriptTestService {
59 59
60 60 private openTestScriptDialog(script: string, scriptType: string,
61 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 63 if (!msg) {
64 64 msg = {
65 65 temperature: 22.4,
... ...
... ... @@ -26,9 +26,6 @@
26 26 <mat-icon class="material-icons">close</mat-icon>
27 27 </button>
28 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 29 <div mat-dialog-content fxFlex style="position: relative;">
33 30 <div class="tb-absolute-fill">
34 31 <div #topPanel class="tb-split tb-split-vertical">
... ... @@ -37,7 +34,24 @@
37 34 <div class="tb-editor-area-title-panel">
38 35 <label translate>rulenode.message</label>
39 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 55 </div>
42 56 </div>
43 57 <div #topRightPanel class="tb-split tb-content">
... ... @@ -45,7 +59,10 @@
45 59 <div class="tb-editor-area-title-panel">
46 60 <label translate>rulenode.metadata</label>
47 61 </div>
48   - TODO: metadataForm
  62 + <tb-key-val-map
  63 + formControlName="metadata"
  64 + titleText="rulenode.metadata">
  65 + </tb-key-val-map>
49 66 </div>
50 67 </div>
51 68 </div>
... ... @@ -55,7 +72,14 @@
55 72 <div class="tb-editor-area-title-panel tb-js-function">
56 73 <label>{{ functionTitle }}</label>
57 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 83 </div>
60 84 </div>
61 85 <div #bottomRightPanel class="tb-split tb-content">
... ... @@ -63,7 +87,15 @@
63 87 <div class="tb-editor-area-title-panel">
64 88 <label translate>rulenode.output</label>
65 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 99 </div>
68 100 </div>
69 101 </div>
... ... @@ -79,7 +111,7 @@
79 111 <span fxFlex></span>
80 112 <button mat-button mat-raised-button color="primary"
81 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 115 {{ 'action.save' | translate }}
84 116 </button>
85 117 <button mat-button color="primary"
... ...
... ... @@ -23,6 +23,10 @@
23 23 overflow-y: auto;
24 24 }
25 25
  26 + .ace_editor {
  27 + font-size: 14px !important;
  28 + }
  29 +
26 30 .tb-content {
27 31 padding-top: 5px;
28 32 padding-left: 5px;
... ...
... ... @@ -21,7 +21,7 @@ import {
21 21 Inject,
22 22 OnInit,
23 23 QueryList,
24   - SkipSelf,
  24 + SkipSelf, ViewChild,
25 25 ViewChildren,
26 26 ViewEncapsulation
27 27 } from '@angular/core';
... ... @@ -29,9 +29,16 @@ import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/mater
29 29 import { Store } from '@ngrx/store';
30 30 import { AppState } from '@core/core.state';
31 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 33 import { Router } from '@angular/router';
34 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 43 export interface NodeScriptTestDialogData {
37 44 script: string;
... ... @@ -40,7 +47,7 @@ export interface NodeScriptTestDialogData {
40 47 functionName: string;
41 48 argNames: string[];
42 49 msg?: any;
43   - metadata?: any;
  50 + metadata?: {[key: string]: string};
44 51 msgType?: string;
45 52 }
46 53
... ... @@ -75,46 +82,46 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
75 82 @ViewChildren('bottomRightPanel')
76 83 bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>;
77 84
  85 + @ViewChild('payloadContent', {static: true}) payloadContent: JsonContentComponent;
  86 +
78 87 nodeScriptTestFormGroup: FormGroup;
79 88
80 89 functionTitle: string;
81 90
82 91 submitted = false;
83 92
  93 + contentTypes = ContentType;
  94 +
84 95 constructor(protected store: Store<AppState>,
85 96 protected router: Router,
86 97 @Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData,
87 98 @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
88 99 public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>,
89   - public fb: FormBuilder) {
  100 + public fb: FormBuilder,
  101 + private ruleChainService: RuleChainService) {
90 102 super(store, router, dialogRef);
91 103 this.functionTitle = this.data.functionTitle;
92 104 }
93 105
94 106 ngOnInit(): void {
95 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 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 127 private initSplitLayout(topPanel: any,
... ... @@ -154,9 +161,54 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
154 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 207 save(): void {
158 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 19 Component,
20 20 ElementRef,
21 21 forwardRef,
22   - Input,
  22 + Input, OnDestroy,
23 23 OnInit,
24 24 ViewChild
25 25 } from '@angular/core';
... ... @@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
29 29 import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
30 30 import { Store } from '@ngrx/store';
31 31 import { AppState } from '@core/core.state';
  32 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
32 33
33 34 @Component({
34 35 selector: 'tb-json-object-edit',
... ... @@ -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 53 @ViewChild('jsonEditor', {static: true})
53 54 jsonEditorElmRef: ElementRef;
54 55
55 56 private jsonEditor: ace.Ace.Editor;
  57 + private editorsResizeCaf: CancelAnimationFrame;
  58 + private editorResizeListener: any;
56 59
57 60 @Input() label: string;
58 61
... ... @@ -95,7 +98,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
95 98 private propagateChange = null;
96 99
97 100 constructor(public elementRef: ElementRef,
98   - protected store: Store<AppState>) {
  101 + protected store: Store<AppState>,
  102 + private raf: RafService) {
99 103 }
100 104
101 105 ngOnInit(): void {
... ... @@ -122,6 +126,28 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
122 126 this.cleanupJsonErrors();
123 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 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 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 196 export const customTranslationsPrefix = 'custom.';
... ...
... ... @@ -232,6 +232,58 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
232 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 287 const ruleNodeClazzHelpLinkMap = {
236 288 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation',
237 289 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields',
... ...
... ... @@ -115,6 +115,9 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
115 115 import { ImageInputComponent } from './components/image-input.component';
116 116 import { FileInputComponent } from './components/file-input.component';
117 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 122 @NgModule({
120 123 providers: [
... ... @@ -175,6 +178,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
175 178 RelationTypeAutocompleteComponent,
176 179 SocialSharePanelComponent,
177 180 JsonObjectEditComponent,
  181 + JsonContentComponent,
178 182 JsFuncComponent,
179 183 FabTriggerDirective,
180 184 FabActionsDirective,
... ... @@ -188,6 +192,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
188 192 JsonFormComponent,
189 193 ImageInputComponent,
190 194 FileInputComponent,
  195 + MessageTypeAutocompleteComponent,
  196 + KeyValMapComponent,
191 197 NospacePipe,
192 198 MillisecondsToTimeStringPipe,
193 199 EnumToArrayPipe,
... ... @@ -276,6 +282,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
276 282 RelationTypeAutocompleteComponent,
277 283 SocialSharePanelComponent,
278 284 JsonObjectEditComponent,
  285 + JsonContentComponent,
279 286 JsFuncComponent,
280 287 FabTriggerDirective,
281 288 FabActionsDirective,
... ... @@ -331,6 +338,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
331 338 JsonFormComponent,
332 339 ImageInputComponent,
333 340 FileInputComponent,
  341 + MessageTypeAutocompleteComponent,
  342 + KeyValMapComponent,
334 343 NospacePipe,
335 344 MillisecondsToTimeStringPipe,
336 345 EnumToArrayPipe,
... ...