Showing
18 changed files
with
1112 additions
and
43 deletions
... | ... | @@ -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" | ... | ... |
... | ... | @@ -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, | ... | ... |