diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index 1f9ade5601..ce7b23c47c 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/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(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); } + public testScript(inputParams: TestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/ruleChain/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + private resolveTargetRuleChains(ruleChainConnections: Array): Observable<{[ruleChainId: string]: RuleChain}> { if (ruleChainConnections && ruleChainConnections.length) { const tasks: Observable[] = []; diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 7955604908..fe11d8461d 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/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, - private dialogService: DialogService, - private translate: TranslateService, - private authService: AuthService) { + constructor(@Inject(Store) private store: Store, + @Inject(DialogService) private dialogService: DialogService, + @Inject(TranslateService) private translate: TranslateService, + @Inject(AuthService) private authService: AuthService) { } intercept(req: HttpRequest, next: HttpHandler): Observable> { diff --git a/ui-ngx/src/app/core/services/script/node-script-test.service.ts b/ui-ngx/src/app/core/services/script/node-script-test.service.ts index d946425698..86213c717f 100644 --- a/ui-ngx/src/app/core/services/script/node-script-test.service.ts +++ b/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 { + msg?: any, metadata?: {[key: string]: string}, msgType?: string): Observable { if (!msg) { msg = { temperature: 22.4, diff --git a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html index c1b20aef19..ee1146368f 100644 --- a/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html +++ b/ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.html @@ -26,9 +26,6 @@ close - - -
@@ -37,7 +34,24 @@
- TODO: payloadForm +
+
+ + +
+ + +
@@ -45,7 +59,10 @@
- TODO: metadataForm + +
@@ -55,7 +72,14 @@
- TODO: funcBodyForm + +
@@ -63,7 +87,15 @@
- TODO: output + +
@@ -79,7 +111,7 @@ + + +
+
+
+ diff --git a/ui-ngx/src/app/shared/components/json-content.component.scss b/ui-ngx/src/app/shared/components/json-content.component.scss new file mode 100644 index 0000000000..7d8c222e12 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-content.component.ts b/ui-ngx/src/app/shared/components/json-content.component.ts new file mode 100644 index 0000000000..e8c61c8bbc --- /dev/null +++ b/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, + 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 = { + 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); + } + } + +} diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts index 0ee763ecc0..1e1a4ec6a9 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts +++ b/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) { + protected store: Store, + 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 { diff --git a/ui-ngx/src/app/shared/components/kv-map.component.html b/ui-ngx/src/app/shared/components/kv-map.component.html new file mode 100644 index 0000000000..da03296492 --- /dev/null +++ b/ui-ngx/src/app/shared/components/kv-map.component.html @@ -0,0 +1,60 @@ + + +
+ +
+ + + + + + + + + +
+ {{noDataText ? noDataText : 'key-val.no-data'}} +
+ +
+
diff --git a/ui-ngx/src/app/shared/components/kv-map.component.scss b/ui-ngx/src/app/shared/components/kv-map.component.scss new file mode 100644 index 0000000000..0507cd4d09 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/shared/components/kv-map.component.ts b/ui-ngx/src/app/shared/components/kv-map.component.ts new file mode 100644 index 0000000000..41df0999c0 --- /dev/null +++ b/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, + 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 = []; + 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); + } +} diff --git a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html new file mode 100644 index 0000000000..3c63fb0168 --- /dev/null +++ b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.html @@ -0,0 +1,43 @@ + + + {{ 'rulenode.message-type' | translate }} + + + + + + + + + {{ 'rulenode.message-type-required' | translate }} + + diff --git a/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts b/ui-ngx/src/app/shared/components/message-type-autocomplete.component.ts new file mode 100644 index 0000000000..1f9b936292 --- /dev/null +++ b/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>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + 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(''), + 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> { + this.searchText = searchText; + const result: Array = []; + 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); + } + +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 9dda11b09f..0bbb06ce2b 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -156,4 +156,41 @@ export const valueTypesMap = new Map( ] ); +export interface ContentTypeData { + name: string; + code: string; +} + +export enum ContentType { + JSON = 'JSON', + TEXT = 'TEXT', + BINARY = 'BINARY' +} + +export const contentTypesMap = new Map( + [ + [ + 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.'; diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index a1225ef8da..b611eca66c 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/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.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', diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index d7773efac0..73657a320e 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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,