Browse Source

Rule Node Test Script

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
db02d42e37
  1. 6
      ui-ngx/src/app/core/http/rule-chain.service.ts
  2. 10
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  3. 4
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  4. 48
      ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html
  5. 4
      ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss
  6. 100
      ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts
  7. 39
      ui-ngx/src/app/shared/components/json-content.component.html
  8. 57
      ui-ngx/src/app/shared/components/json-content.component.scss
  9. 275
      ui-ngx/src/app/shared/components/json-content.component.ts
  10. 32
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  11. 60
      ui-ngx/src/app/shared/components/kv-map.component.html
  12. 41
      ui-ngx/src/app/shared/components/kv-map.component.scss
  13. 160
      ui-ngx/src/app/shared/components/kv-map.component.ts
  14. 43
      ui-ngx/src/app/shared/components/message-type-autocomplete.component.html
  15. 178
      ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts
  16. 37
      ui-ngx/src/app/shared/models/constants.ts
  17. 52
      ui-ngx/src/app/shared/models/rule-node.models.ts
  18. 9
      ui-ngx/src/app/shared/shared.module.ts

6
ui-ngx/src/app/core/http/rule-chain.service.ts

@ -31,7 +31,7 @@ import { ComponentDescriptorService } from './component-descriptor.service';
import {
IRuleNodeConfigurationComponent,
LinkLabel,
RuleNodeComponentDescriptor
RuleNodeComponentDescriptor, TestScriptInputParams, TestScriptResult
} from '@app/shared/models/rule-node.models';
import { ResourcesService } from '../services/resources.service';
import { catchError, map, mergeMap } from 'rxjs/operators';
@ -175,6 +175,10 @@ export class RuleChainService {
return this.http.get<DebugRuleNodeEventBody>(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config));
}
public testScript(inputParams: TestScriptInputParams, config?: RequestConfig): Observable<TestScriptResult> {
return this.http.post<TestScriptResult>('/api/ruleChain/testScript', inputParams, defaultHttpOptionsFromConfig(config));
}
private resolveTargetRuleChains(ruleChainConnections: Array<RuleChainConnectionInfo>): Observable<{[ruleChainId: string]: RuleChain}> {
if (ruleChainConnections && ruleChainConnections.length) {
const tasks: Observable<RuleChain>[] = [];

10
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -23,7 +23,7 @@ import {
HttpResponseBase
} from '@angular/common/http';
import { Observable } from 'rxjs/internal/Observable';
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { AuthService } from '../auth/auth.service';
import { Constants } from '../../shared/models/constants';
import { InterceptorHttpParams } from './interceptor-http-params';
@ -52,10 +52,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private activeRequests = 0;
constructor(private store: Store<AppState>,
private dialogService: DialogService,
private translate: TranslateService,
private authService: AuthService) {
constructor(@Inject(Store) private store: Store<AppState>,
@Inject(DialogService) private dialogService: DialogService,
@Inject(TranslateService) private translate: TranslateService,
@Inject(AuthService) private authService: AuthService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

4
ui-ngx/src/app/core/services/script/node-script-test.service.ts

@ -36,7 +36,7 @@ export class NodeScriptTestService {
return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
switchMap((debugIn) => {
let msg: any;
let metadata: any;
let metadata: {[key: string]: string};
let msgType: string;
if (debugIn) {
if (debugIn.data) {
@ -59,7 +59,7 @@ export class NodeScriptTestService {
private openTestScriptDialog(script: string, scriptType: string,
functionTitle: string, functionName: string, argNames: string[],
msg?: any, metadata?: any, msgType?: string): Observable<string> {
msg?: any, metadata?: {[key: string]: string}, msgType?: string): Observable<string> {
if (!msg) {
msg = {
temperature: 22.4,

48
ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html

@ -26,9 +26,6 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content fxFlex style="position: relative;">
<div class="tb-absolute-fill">
<div #topPanel class="tb-split tb-split-vertical">
@ -37,7 +34,24 @@
<div class="tb-editor-area-title-panel">
<label translate>rulenode.message</label>
</div>
TODO: payloadForm
<div formGroupName="payload" fxLayout="column" style="height: 100%;">
<div fxLayout="row">
<tb-message-type-autocomplete
style="margin-bottom: 0px; min-width: 300px;"
formControlName="msgType"
required>
</tb-message-type-autocomplete>
</div>
<tb-json-content
#payloadContent
fxFlex
formControlName="msg"
label="{{ 'rulenode.message' | translate }}"
[contentType]="contentTypes.JSON"
validateContent
[fillHeight]="true">
</tb-json-content>
</div>
</div>
</div>
<div #topRightPanel class="tb-split tb-content">
@ -45,7 +59,10 @@
<div class="tb-editor-area-title-panel">
<label translate>rulenode.metadata</label>
</div>
TODO: metadataForm
<tb-key-val-map
formControlName="metadata"
titleText="rulenode.metadata">
</tb-key-val-map>
</div>
</div>
</div>
@ -55,7 +72,14 @@
<div class="tb-editor-area-title-panel tb-js-function">
<label>{{ functionTitle }}</label>
</div>
TODO: funcBodyForm
<tb-js-func
formControlName="script"
functionName="{{ data.functionName }}"
[functionArgs]="data.argNames"
[validationArgs]="[data.msg, data.metadata, data.msgType]"
resultType="object"
[fillHeight]="true">
</tb-js-func>
</div>
</div>
<div #bottomRightPanel class="tb-split tb-content">
@ -63,7 +87,15 @@
<div class="tb-editor-area-title-panel">
<label translate>rulenode.output</label>
</div>
TODO: output
<tb-json-content
fxFlex
formControlName="output"
label="{{ 'rulenode.output' | translate }}"
[contentType]="contentTypes.JSON"
validateContent="false"
readonly="true"
[fillHeight]="true">
</tb-json-content>
</div>
</div>
</div>
@ -79,7 +111,7 @@
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('funcBody').invalid || !nodeScriptTestFormGroup.get('funcBody').dirty">
[disabled]="(isLoading$ | async) || nodeScriptTestFormGroup.get('script').invalid || !nodeScriptTestFormGroup.get('script').dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"

4
ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.scss

@ -23,6 +23,10 @@
overflow-y: auto;
}
.ace_editor {
font-size: 14px !important;
}
.tb-content {
padding-top: 5px;
padding-left: 5px;

100
ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts

@ -21,7 +21,7 @@ import {
Inject,
OnInit,
QueryList,
SkipSelf,
SkipSelf, ViewChild,
ViewChildren,
ViewEncapsulation
} from '@angular/core';
@ -29,9 +29,16 @@ import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/mater
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { combineLatest } from 'rxjs';
import { combineLatest, never, Observable, of, throwError, NEVER } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { ContentType } from '@shared/models/constants';
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { JsonContentComponent } from '@shared/components/json-content.component';
import { TestScriptInputParams } from '@shared/models/rule-node.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { map, mergeMap } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
export interface NodeScriptTestDialogData {
script: string;
@ -40,7 +47,7 @@ export interface NodeScriptTestDialogData {
functionName: string;
argNames: string[];
msg?: any;
metadata?: any;
metadata?: {[key: string]: string};
msgType?: string;
}
@ -75,46 +82,46 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
@ViewChildren('bottomRightPanel')
bottomRightPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ViewChild('payloadContent', {static: true}) payloadContent: JsonContentComponent;
nodeScriptTestFormGroup: FormGroup;
functionTitle: string;
submitted = false;
contentTypes = ContentType;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: NodeScriptTestDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<NodeScriptTestDialogComponent, string>,
public fb: FormBuilder) {
public fb: FormBuilder,
private ruleChainService: RuleChainService) {
super(store, router, dialogRef);
this.functionTitle = this.data.functionTitle;
}
ngOnInit(): void {
this.nodeScriptTestFormGroup = this.fb.group({
funcBody: ['', [Validators.required]]
payload: this.fb.group({
msgType: [this.data.msgType, [Validators.required]],
msg: [js_beautify(JSON.stringify(this.data.msg), {indent_size: 4}), []],
}),
metadata: [this.data.metadata, [Validators.required]],
script: [this.data.script, []],
output: ['', []]
});
}
ngAfterViewInit(): void {
/* combineLatest(this.topPanelElmRef.changes,
this.topLeftPanelElmRef.changes,
this.topRightPanelElmRef.changes,
this.bottomPanelElmRef.changes,
this.bottomLeftPanelElmRef.changes,
this.bottomRightPanelElmRef.changes).subscribe(() => {
if (this.topPanelElmRef.length && this.topLeftPanelElmRef.length &&
this.topRightPanelElmRef.length && this.bottomPanelElmRef.length &&
this.bottomLeftPanelElmRef.length && this.bottomRightPanelElmRef.length) {*/
this.initSplitLayout(this.topPanelElmRef.first.nativeElement,
this.topLeftPanelElmRef.first.nativeElement,
this.topRightPanelElmRef.first.nativeElement,
this.bottomPanelElmRef.first.nativeElement,
this.bottomLeftPanelElmRef.first.nativeElement,
this.bottomRightPanelElmRef.first.nativeElement);
// }
//});
this.initSplitLayout(this.topPanelElmRef.first.nativeElement,
this.topLeftPanelElmRef.first.nativeElement,
this.topRightPanelElmRef.first.nativeElement,
this.bottomPanelElmRef.first.nativeElement,
this.bottomLeftPanelElmRef.first.nativeElement,
this.bottomRightPanelElmRef.first.nativeElement);
}
private initSplitLayout(topPanel: any,
@ -154,9 +161,54 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
this.dialogRef.close(null);
}
test(): void {
this.testNodeScript().subscribe((output) => {
this.nodeScriptTestFormGroup.get('output').setValue(js_beautify(output, {indent_size: 4}));
});
}
private testNodeScript(): Observable<string> {
if (this.checkInputParamErrors()) {
const inputParams: TestScriptInputParams = {
argNames: this.data.argNames,
scriptType: this.data.scriptType,
msgType: this.nodeScriptTestFormGroup.get('payload').get('msgType').value,
msg: this.nodeScriptTestFormGroup.get('payload').get('msg').value,
metadata: this.nodeScriptTestFormGroup.get('metadata').value,
script: this.nodeScriptTestFormGroup.get('script').value
};
return this.ruleChainService.testScript(inputParams).pipe(
mergeMap((result) => {
if (result.error) {
this.store.dispatch(new ActionNotificationShow(
{
message: result.error,
type: 'error'
}));
return NEVER;
} else {
return of(result.output);
}
})
);
} else {
return NEVER;
}
}
private checkInputParamErrors(): boolean {
this.payloadContent.validateOnSubmit();
if (!this.nodeScriptTestFormGroup.get('payload').valid) {
return false;
}
return true;
}
save(): void {
this.submitted = true;
const script: string = this.nodeScriptTestFormGroup.get('funcBody').value;
this.dialogRef.close(script);
this.testNodeScript().subscribe(() => {
this.nodeScriptTestFormGroup.get('script').markAsPristine();
this.dialogRef.close(this.nodeScriptTestFormGroup.get('script').value);
});
}
}

39
ui-ngx/src/app/shared/components/json-content.component.html

@ -0,0 +1,39 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar">
<label class="tb-title no-padding">{{ label }}</label>
<span fxFlex></span>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()">
{{'js-func.tidy' | translate }}
</button>
<button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
class="tb-mat-32"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div id="tb-json-panel" tb-toast toastTarget="jsonContentEditor"
class="tb-json-content-panel" fxLayout="column">
<div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

57
ui-ngx/src/app/shared/components/json-content.component.scss

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
position: relative;
.fill-height {
height: 100%;
}
}
.tb-json-content-toolbar {
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
}
}
}
.tb-json-content-panel {
height: 100%;
margin-left: 15px;
border: 1px solid #c0c0c0;
#tb-json-input {
width: 100%;
min-width: 200px;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
}
}

275
ui-ngx/src/app/shared/components/json-content.component.ts

@ -0,0 +1,275 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ElementRef,
forwardRef,
Input,
OnChanges,
OnInit,
ViewChild,
SimpleChanges,
OnDestroy
} from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import * as ace from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ContentType, contentTypesMap } from '@shared/models/constants';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
@Component({
selector: 'tb-json-content',
templateUrl: './json-content.component.html',
styleUrls: ['./json-content.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => JsonContentComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => JsonContentComponent),
multi: true,
}
]
})
export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy {
@ViewChild('jsonEditor', {static: true})
jsonEditorElmRef: ElementRef;
private jsonEditor: ace.Ace.Editor;
private editorsResizeCaf: CancelAnimationFrame;
private editorResizeListener: any;
@Input() label: string;
@Input() contentType: ContentType;
@Input() disabled: boolean;
@Input() fillHeight: boolean;
@Input() editorStyle: {[klass: string]: any};
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
}
@Input()
set readonly(value: boolean) {
this.readonlyValue = coerceBooleanProperty(value);
}
private validateContentValue: boolean;
get validateContent(): boolean {
return this.validateContentValue;
}
@Input()
set validateContent(value: boolean) {
this.validateContentValue = coerceBooleanProperty(value);
}
fullscreen = false;
contentBody: string;
contentValid: boolean;
validationError: string;
errorShowed = false;
private propagateChange = null;
constructor(public elementRef: ElementRef,
protected store: Store<AppState>,
private raf: RafService) {
}
ngOnInit(): void {
const editorElement = this.jsonEditorElmRef.nativeElement;
let mode = 'text';
if (this.contentType) {
mode = contentTypesMap.get(this.contentType).code;
}
let editorOptions: Partial<ace.Ace.EditorOptions> = {
mode: `ace/mode/${mode}`,
theme: 'ace/theme/github',
showGutter: true,
showPrintMargin: false,
readOnly: this.readonly
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
this.jsonEditor = ace.edit(editorElement, editorOptions);
this.jsonEditor.session.setUseWrapMode(true);
this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1);
this.jsonEditor.on('change', () => {
this.cleanupJsonErrors();
this.updateView();
});
this.editorResizeListener = this.onAceEditorResize.bind(this);
// @ts-ignore
addResizeListener(editorElement, this.editorResizeListener);
}
ngOnDestroy(): void {
if (this.editorResizeListener) {
const editorElement = this.jsonEditorElmRef.nativeElement;
// @ts-ignore
removeResizeListener(editorElement, this.editorResizeListener);
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.jsonEditor.resize();
this.jsonEditor.renderer.updateFull();
});
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'contentType') {
if (this.jsonEditor) {
let mode = 'text';
if (this.contentType) {
mode = contentTypesMap.get(this.contentType).code;
}
this.jsonEditor.session.setMode(`ace/mode/${mode}`);
}
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
public validate(c: FormControl) {
return (this.contentValid) ? null : {
contentBody: {
valid: false,
},
};
}
validateOnSubmit(): void {
if (!this.readonly) {
this.cleanupJsonErrors();
this.contentValid = true;
this.propagateChange(this.contentBody);
this.contentValid = this.doValidate();
this.propagateChange(this.contentBody);
}
}
private doValidate(): boolean {
try {
if (this.validateContent && this.contentType === ContentType.JSON) {
JSON.parse(this.contentBody);
}
return true;
} catch (ex) {
let errorInfo = 'Error:';
if (ex.name) {
errorInfo += ' ' + ex.name + ':';
}
if (ex.message) {
errorInfo += ' ' + ex.message;
}
this.store.dispatch(new ActionNotificationShow(
{
message: errorInfo,
type: 'error',
target: 'jsonContentEditor',
verticalPosition: 'bottom',
horizontalPosition: 'left'
}));
this.errorShowed = true;
return false;
}
}
cleanupJsonErrors(): void {
if (this.errorShowed) {
this.store.dispatch(new ActionNotificationHide(
{
target: 'jsonContentEditor'
}));
this.errorShowed = false;
}
}
writeValue(value: string): void {
this.contentBody = value;
this.contentValid = true;
if (this.jsonEditor) {
this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1);
// this.jsonEditor.
}
}
updateView() {
const editorValue = this.jsonEditor.getValue();
if (this.contentBody !== editorValue) {
this.contentBody = editorValue;
this.contentValid = true;
this.propagateChange(this.contentBody);
}
}
beautifyJson() {
const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60});
this.jsonEditor.setValue(res ? res : '', -1);
this.updateView();
}
onFullscreen() {
if (this.jsonEditor) {
setTimeout(() => {
this.jsonEditor.resize();
}, 0);
}
}
}

32
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -19,7 +19,7 @@ import {
Component,
ElementRef,
forwardRef,
Input,
Input, OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
@Component({
selector: 'tb-json-object-edit',
@ -47,12 +48,14 @@ import { AppState } from '@core/core.state';
}
]
})
export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator {
export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy {
@ViewChild('jsonEditor', {static: true})
jsonEditorElmRef: ElementRef;
private jsonEditor: ace.Ace.Editor;
private editorsResizeCaf: CancelAnimationFrame;
private editorResizeListener: any;
@Input() label: string;
@ -95,7 +98,8 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
private propagateChange = null;
constructor(public elementRef: ElementRef,
protected store: Store<AppState>) {
protected store: Store<AppState>,
private raf: RafService) {
}
ngOnInit(): void {
@ -122,6 +126,28 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.cleanupJsonErrors();
this.updateView();
});
this.editorResizeListener = this.onAceEditorResize.bind(this);
// @ts-ignore
addResizeListener(editorElement, this.editorResizeListener);
}
ngOnDestroy(): void {
if (this.editorResizeListener) {
const editorElement = this.jsonEditorElmRef.nativeElement;
// @ts-ignore
removeResizeListener(editorElement, this.editorResizeListener);
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.jsonEditor.resize();
this.jsonEditor.renderer.updateFull();
});
}
registerOnChange(fn: any): void {

60
ui-ngx/src/app/shared/components/kv-map.component.html

@ -0,0 +1,60 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section fxLayout="column" class="tb-kv-map" [formGroup]="kvListFormGroup">
<label translate class="tb-title no-padding">{{ titleText }}</label>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;"
formArrayName="keyVals"
*ngFor="let keyValControl of keyValsFormArray().controls; let $index = index">
<mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block"
style="max-height: 40px;">
<mat-label></mat-label>
<input [formControl]="keyValControl.get('key')" matInput required
placeholder="{{ (keyPlaceholderText ? keyPlaceholderText : 'key-val.key') | translate }}"/>
</mat-form-field>
<mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block"
style="max-height: 40px;">
<mat-label></mat-label>
<input [formControl]="keyValControl.get('value')" matInput required
placeholder="{{ (valuePlaceholderText ? valuePlaceholderText : 'key-val.value') | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button color="primary"
[fxShow]="!disabled"
type="button"
(click)="removeKeyVal($index)"
[disabled]="isLoading$ | async"
matTooltip="{{ 'key-val.remove-entry' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<span [fxShow]="!keyValsFormArray().length"
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}"
class="no-data-found" translate>{{noDataText ? noDataText : 'key-val.no-data'}}</span>
<div style="margin-top: 8px;">
<button mat-button mat-raised-button color="primary"
[fxShow]="!disabled"
[disabled]="isLoading$ | async"
(click)="addKeyVal()"
type="button"
matTooltip="{{ 'key-val.add-entry' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
</div>
</section>

41
ui-ngx/src/app/shared/components/kv-map.component.scss

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-kv-map {
span.no-data-found {
position: relative;
display: flex;
height: 40px;
text-transform: uppercase;
&.disabled {
color: rgba(0, 0, 0, .38);
}
}
}
}
:host ::ng-deep {
.mat-form-field-wrapper {
padding-bottom: 0;
}
.mat-form-field-infix {
border-top: 0;
}
.mat-form-field-underline {
bottom: 0;
}
}

160
ui-ngx/src/app/shared/components/kv-map.component.ts

@ -0,0 +1,160 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor, FormArray,
FormBuilder, FormControl,
FormGroup, NG_VALIDATORS,
NG_VALUE_ACCESSOR, Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models';
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityService } from '@core/http/entity.service';
import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Subscription } from 'rxjs';
@Component({
selector: 'tb-key-val-map',
templateUrl: './kv-map.component.html',
styleUrls: ['./kv-map.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyValMapComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => KeyValMapComponent),
multi: true,
}
]
})
export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator {
@Input() disabled: boolean;
@Input() titleText: string;
@Input() keyPlaceholderText: string;
@Input() valuePlaceholderText: string;
@Input() noDataText: string;
kvListFormGroup: FormGroup;
private propagateChange = null;
private valueChangeSubscription: Subscription = null;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.kvListFormGroup = this.fb.group({});
this.kvListFormGroup.addControl('keyVals',
this.fb.array([]));
}
keyValsFormArray(): FormArray {
return this.kvListFormGroup.get('keyVals') as FormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.kvListFormGroup.disable({emitEvent: false});
} else {
this.kvListFormGroup.enable({emitEvent: false});
}
}
writeValue(keyValMap: {[key: string]: string}): void {
if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe();
}
const keyValsControls: Array<AbstractControl> = [];
if (keyValMap) {
for (const property of Object.keys(keyValMap)) {
if (Object.prototype.hasOwnProperty.call(keyValMap, property)) {
keyValsControls.push(this.fb.group({
key: [property, [Validators.required]],
value: [keyValMap[property], [Validators.required]]
}));
}
}
}
this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls));
this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
public removeKeyVal(index: number) {
(this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index);
}
public addKeyVal() {
const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray;
keyValsFormArray.push(this.fb.group({
key: ['', [Validators.required]],
value: ['', [Validators.required]]
}));
}
public validate(c: FormControl) {
const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value;
let valid = true;
for (const entry of kvList) {
if (!entry.key || !entry.value) {
valid = false;
break;
}
}
return (valid) ? null : {
keyVals: {
valid: false,
},
};
}
private updateModel() {
const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value;
const keyValMap: {[key: string]: string} = {};
kvList.forEach((entry) => {
keyValMap[entry.key] = entry.value;
});
this.propagateChange(keyValMap);
}
}

43
ui-ngx/src/app/shared/components/message-type-autocomplete.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="messageTypeFormGroup" class="mat-block">
<mat-label>{{ 'rulenode.message-type' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'rulenode.select-message-type' | translate }}"
#messageTypeInput
formControlName="messageType"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="messageTypeAutocomplete">
<button *ngIf="messageTypeFormGroup.get('messageType').value && !disabled"
type="button"
matSuffix mat-button mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
class="tb-autocomplete"
#messageTypeAutocomplete="matAutocomplete"
[displayWith]="displayMessageTypeFn">
<mat-option *ngFor="let messageType of filteredMessageTypes | async" [value]="messageType">
<span [innerHTML]="displayMessageTypeFn(messageType) | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="messageTypeFormGroup.get('messageType').hasError('required')">
{{ 'rulenode.message-type-required' | translate }}
</mat-error>
</mat-form-field>

178
ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts

@ -0,0 +1,178 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, mergeMap, startWith, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { MessageType, messageTypeNames } from '@shared/models/rule-node.models';
@Component({
selector: 'tb-message-type-autocomplete',
templateUrl: './message-type-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MessageTypeAutocompleteComponent),
multi: true
}]
})
export class MessageTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
messageTypeFormGroup: FormGroup;
modelValue: string | null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ViewChild('messageTypeInput', {static: true}) messageTypeInput: ElementRef;
filteredMessageTypes: Observable<Array<MessageType | string>>;
searchText = '';
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
private fb: FormBuilder) {
this.messageTypeFormGroup = this.fb.group({
messageType: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredMessageTypes = this.messageTypeFormGroup.get('messageType').valueChanges
.pipe(
tap(value => {
this.updateView(value);
}),
startWith<string | MessageType>(''),
map(value => value ? value : ''),
mergeMap(messageType => this.fetchMessageTypes(messageType) )
);
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.messageTypeFormGroup.disable({emitEvent: false});
} else {
this.messageTypeFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | null): void {
this.searchText = '';
this.modelValue = value;
let res: MessageType | string = null;
if (value) {
if (Object.values(MessageType).includes(value)) {
res = MessageType[value];
} else {
res = value;
}
}
this.messageTypeFormGroup.get('messageType').patchValue(res, {emitEvent: false});
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.messageTypeFormGroup.get('messageType').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
updateView(value: MessageType | string | null) {
let res: string = null;
if (value) {
if (Object.values(MessageType).includes(value)) {
res = MessageType[value];
} else {
res = value;
}
}
if (this.modelValue !== res) {
this.modelValue = res;
this.propagateChange(this.modelValue);
}
}
displayMessageTypeFn(messageType?: MessageType | string): string | undefined {
if (messageType) {
if (Object.values(MessageType).includes(messageType)) {
return messageTypeNames.get(MessageType[messageType]);
} else {
return messageType;
}
}
return undefined;
}
fetchMessageTypes(searchText?: string): Observable<Array<MessageType | string>> {
this.searchText = searchText;
const result: Array<MessageType | string> = [];
messageTypeNames.forEach((value, key) => {
if (value.toUpperCase().includes(searchText.toUpperCase())) {
result.push(key);
}
});
if (result.length) {
return of(result);
} else {
return of([searchText]);
}
}
clear() {
this.messageTypeFormGroup.get('messageType').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.messageTypeInput.nativeElement.blur();
this.messageTypeInput.nativeElement.focus();
}, 0);
}
}

37
ui-ngx/src/app/shared/models/constants.ts

@ -156,4 +156,41 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
]
);
export interface ContentTypeData {
name: string;
code: string;
}
export enum ContentType {
JSON = 'JSON',
TEXT = 'TEXT',
BINARY = 'BINARY'
}
export const contentTypesMap = new Map<ContentType, ContentTypeData>(
[
[
ContentType.JSON,
{
name: 'content-type.json',
code: 'json'
}
],
[
ContentType.TEXT,
{
name: 'content-type.text',
code: 'text'
}
],
[
ContentType.BINARY,
{
name: 'content-type.binary',
code: 'text'
}
]
]
);
export const customTranslationsPrefix = 'custom.';

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

@ -232,6 +232,58 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
configurationDescriptor?: RuleNodeConfigurationDescriptor;
}
export interface TestScriptInputParams {
script: string;
scriptType: string;
argNames: string[];
msg: string;
metadata: {[key: string]: string};
msgType: string;
}
export interface TestScriptResult {
output: string;
error: string;
}
export enum MessageType {
POST_ATTRIBUTES_REQUEST = 'POST_ATTRIBUTES_REQUEST',
POST_TELEMETRY_REQUEST = 'POST_TELEMETRY_REQUEST',
TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST',
RPC_CALL_FROM_SERVER_TO_DEVICE = 'RPC_CALL_FROM_SERVER_TO_DEVICE',
ACTIVITY_EVENT = 'ACTIVITY_EVENT',
INACTIVITY_EVENT = 'INACTIVITY_EVENT',
CONNECT_EVENT = 'CONNECT_EVENT',
DISCONNECT_EVENT = 'DISCONNECT_EVENT',
ENTITY_CREATED = 'ENTITY_CREATED',
ENTITY_UPDATED = 'ENTITY_UPDATED',
ENTITY_DELETED = 'ENTITY_DELETED',
ENTITY_ASSIGNED = 'ENTITY_ASSIGNED',
ENTITY_UNASSIGNED = 'ENTITY_UNASSIGNED',
ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED',
ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED'
}
export const messageTypeNames = new Map<MessageType, string>(
[
[MessageType.POST_ATTRIBUTES_REQUEST, 'Post attributes'],
[MessageType.POST_TELEMETRY_REQUEST, 'Post telemetry'],
[MessageType.TO_SERVER_RPC_REQUEST, 'RPC Request from Device'],
[MessageType.RPC_CALL_FROM_SERVER_TO_DEVICE, 'RPC Request to Device'],
[MessageType.ACTIVITY_EVENT, 'Activity Event'],
[MessageType.INACTIVITY_EVENT, 'Inactivity Event'],
[MessageType.CONNECT_EVENT, 'Connect Event'],
[MessageType.DISCONNECT_EVENT, 'Disconnect Event'],
[MessageType.ENTITY_CREATED, 'Entity Created'],
[MessageType.ENTITY_UPDATED, 'Entity Updated'],
[MessageType.ENTITY_DELETED, 'Entity Deleted'],
[MessageType.ENTITY_ASSIGNED, 'Entity Assigned'],
[MessageType.ENTITY_UNASSIGNED, 'Entity Unassigned'],
[MessageType.ATTRIBUTES_UPDATED, 'Attributes Updated'],
[MessageType.ATTRIBUTES_DELETED, 'Attributes Deleted']
]
);
const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation',
'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields',

9
ui-ngx/src/app/shared/shared.module.ts

@ -115,6 +115,9 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
import { ImageInputComponent } from './components/image-input.component';
import { FileInputComponent } from './components/file-input.component';
import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-script-test-dialog.component';
import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component';
import { JsonContentComponent } from './components/json-content.component';
import { KeyValMapComponent } from './components/kv-map.component';
@NgModule({
providers: [
@ -175,6 +178,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
JsonContentComponent,
JsFuncComponent,
FabTriggerDirective,
FabActionsDirective,
@ -188,6 +192,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
JsonFormComponent,
ImageInputComponent,
FileInputComponent,
MessageTypeAutocompleteComponent,
KeyValMapComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -276,6 +282,7 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
RelationTypeAutocompleteComponent,
SocialSharePanelComponent,
JsonObjectEditComponent,
JsonContentComponent,
JsFuncComponent,
FabTriggerDirective,
FabActionsDirective,
@ -331,6 +338,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc
JsonFormComponent,
ImageInputComponent,
FileInputComponent,
MessageTypeAutocompleteComponent,
KeyValMapComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

Loading…
Cancel
Save