From 516ae3d8142c4c554cd20499bc6a2774300b48eb Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Mon, 2 Mar 2020 17:55:29 +0200 Subject: [PATCH] Create ui to support attribute type JSON (#2471) --- .../attribute/attribute-table.component.html | 2 +- .../json-object-edit-dialog.component.html | 57 +++++++++++ .../json-object-edit-dialog.component.ts | 63 ++++++++++++ .../directives/tb-json-to-string.directive.ts | 96 +++++++++++++++++++ .../components/json-content.component.html | 6 +- .../components/json-content.component.ts | 8 +- .../json-object-edit.component.html | 10 +- .../json-object-edit.component.scss | 19 ++++ .../components/json-object-edit.component.ts | 16 ++++ .../components/value-input.component.html | 16 ++++ .../components/value-input.component.ts | 34 ++++++- ui-ngx/src/app/shared/models/constants.ts | 10 +- ui-ngx/src/app/shared/pipe/tbJson.pipe.ts | 30 ++++++ ui-ngx/src/app/shared/shared.module.ts | 13 ++- .../assets/locale/locale.constant-en_US.json | 10 +- .../assets/locale/locale.constant-ru_RU.json | 4 +- .../assets/locale/locale.constant-uk_UA.json | 4 +- 17 files changed, 384 insertions(+), 14 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html create mode 100644 ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts create mode 100644 ui-ngx/src/app/shared/pipe/tbJson.pipe.ts diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 4e40fdbe90..774fc805da 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -168,7 +168,7 @@ class="tb-value-cell" (click)="editAttribute($event, attribute)">
- {{attribute.value}} + {{attribute.value | tbJson}} edit diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html new file mode 100644 index 0000000000..07098ea49c --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html @@ -0,0 +1,57 @@ + +
+ +

{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}

+ + +
+ + +
+
+
+ + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts new file mode 100644 index 0000000000..5fc3882a35 --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts @@ -0,0 +1,63 @@ +/// +/// Copyright © 2016-2020 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, Inject, OnInit} from "@angular/core"; +import {DialogComponent} from "@shared/components/dialog.component"; +import {Store} from "@ngrx/store"; +import {AppState} from "@core/core.state"; +import {Router} from "@angular/router"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {FormBuilder, FormGroup} from "@angular/forms"; + +export interface JsonObjectEdittDialogData { + jsonValue: Object; + title?: string; +} + +@Component({ + selector: 'tb-object-edit-dialog', + templateUrl: './json-object-edit-dialog.component.html', + styleUrls: [] +}) +export class JsonObjectEditDialogComponent extends DialogComponent + implements OnInit { + + jsonFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.jsonFormGroup = this.fb.group({ + json: [this.data.jsonValue, []] + }); + } + + cancel(): void { + this.dialogRef.close(undefined); + } + + add(): void { + this.dialogRef.close(this.jsonFormGroup.get('json').value); + } +} diff --git a/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts new file mode 100644 index 0000000000..cf7cd0a0bd --- /dev/null +++ b/ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 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 {Directive, ElementRef, forwardRef, HostListener, Renderer2, SkipSelf} from "@angular/core"; +import { + ControlValueAccessor, + FormControl, FormGroupDirective, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, NgForm, + ValidationErrors, + Validator +} from "@angular/forms"; +import {ErrorStateMatcher} from "@angular/material/core"; + +@Directive({ + selector: '[tb-json-to-string]', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => TbJsonToStringDirective), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => TbJsonToStringDirective), + multi: true, + }, + { + provide: ErrorStateMatcher, + useExisting: TbJsonToStringDirective + }] +}) + +export class TbJsonToStringDirective implements ControlValueAccessor, Validator, ErrorStateMatcher { + private propagateChange = null; + private parseError: boolean; + private data: any; + + @HostListener('input', ['$event.target.value']) input(newValue: any): void { + try { + this.data = JSON.parse(newValue); + this.parseError = false; + } catch (e) { + this.parseError = true; + } + + this.propagateChange(this.data); + } + + constructor(private render: Renderer2, + private element: ElementRef, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher) { + + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.parseError); + return originalErrorState || customErrorState; + } + + validate(c: FormControl): ValidationErrors { + return (!this.parseError) ? null : { + invalidJSON: { + valid: false + } + }; + } + + writeValue(obj: any): void { + if (obj) { + this.data = obj; + this.parseError = false; + this.render.setProperty(this.element.nativeElement, 'value', JSON.stringify(obj)); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } +} diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 9daa94a137..d072c674f4 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -22,9 +22,13 @@ + +
+
+ + value.json-value + + + + {{ (requiredText ? requiredText : 'value.json-value-required') | translate }} + + + {{ 'value.json-value-invalid' | translate }} + + +
diff --git a/ui-ngx/src/app/shared/components/value-input.component.ts b/ui-ngx/src/app/shared/components/value-input.component.ts index d6c61a2438..0913aefe12 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.ts +++ b/ui-ngx/src/app/shared/components/value-input.component.ts @@ -17,6 +17,12 @@ import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgForm } from '@angular/forms'; import { ValueType, valueTypesMap } from '@shared/models/constants'; +import { isObject } from "@core/utils"; +import { MatDialog } from "@angular/material/dialog"; +import { + JsonObjectEditDialogComponent, + JsonObjectEdittDialogData +} from "@shared/components/dialog/json-object-edit-dialog.component"; @Component({ selector: 'tb-value-input', @@ -50,13 +56,35 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor { private propagateChange = null; - constructor() { + constructor( + public dialog: MatDialog, + ) { } ngOnInit(): void { } + openEditJSONDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(JsonObjectEditDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonValue: this.modelValue + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.modelValue = res; + this.inputForm.control.patchValue({'value': this.modelValue}); + } + } + ); + } + registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -78,6 +106,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor { } else { this.valueType = ValueType.DOUBLE; } + } else if (isObject(this.modelValue)) { + this.valueType = ValueType.JSON; } else { this.valueType = ValueType.STRING; } @@ -94,6 +124,8 @@ export class ValueInputComponent implements OnInit, ControlValueAccessor { onValueTypeChanged() { if (this.valueType === ValueType.BOOLEAN) { this.modelValue = false; + } if (this.valueType === ValueType.JSON) { + this.modelValue = {}; } else { this.modelValue = null; } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 39235f5f45..4c9612489a 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -121,7 +121,8 @@ export enum ValueType { STRING = 'STRING', INTEGER = 'INTEGER', DOUBLE = 'DOUBLE', - BOOLEAN = 'BOOLEAN' + BOOLEAN = 'BOOLEAN', + JSON = 'JSON' } export const valueTypesMap = new Map( @@ -153,6 +154,13 @@ export const valueTypesMap = new Map( name: 'value.boolean', icon: 'mdi:checkbox-marked-outline' } + ], + [ + ValueType.JSON, + { + name: 'value.json', + icon: 'mdi:json' + } ] ] ); diff --git a/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts new file mode 100644 index 0000000000..c31cf161ab --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/tbJson.pipe.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2020 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 {Pipe, PipeTransform} from '@angular/core'; +import {isObject, isNumber} from "@core/utils"; + +@Pipe({name: 'tbJson'}) +export class TbJsonPipe implements PipeTransform { + transform(value: any): string { + if (isObject(value)) { + return JSON.stringify(value); + } else if (isNumber(value)) { + return value.toString(); + } + return value; + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 99fb460f7d..1df4469fb2 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -104,6 +104,7 @@ import { TbErrorComponent } from '@shared/components/tb-error.component'; import { EntityTypeListComponent } from '@shared/components/entity/entity-type-list.component'; import { EntitySubTypeListComponent } from '@shared/components/entity/entity-subtype-list.component'; import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { TbJsonPipe } from "@shared/pipe/tbJson.pipe"; import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component'; import { MatChipDraggableDirective } from '@shared/components/mat-chip-draggable.directive'; import { ColorInputComponent } from '@shared/components/color-input.component'; @@ -124,6 +125,8 @@ import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; import { TbHotkeysDirective } from '@shared/components/hotkeys.directive'; import { NavTreeComponent } from '@shared/components/nav-tree.component'; import { LedLightComponent } from '@shared/components/led-light.component'; +import { TbJsonToStringDirective } from "@shared/components/directives/tb-json-to-string.directive"; +import { JsonObjectEditDialogComponent } from "@shared/components/dialog/json-object-edit-dialog.component"; @NgModule({ providers: [ @@ -132,6 +135,7 @@ import { LedLightComponent } from '@shared/components/led-light.component'; EnumToArrayPipe, HighlightPipe, TruncatePipe, + TbJsonPipe, { provide: FlowInjectionToken, useValue: Flow @@ -202,7 +206,10 @@ import { LedLightComponent } from '@shared/components/led-light.component'; EnumToArrayPipe, HighlightPipe, TruncatePipe, - KeyboardShortcutPipe + TbJsonPipe, + KeyboardShortcutPipe, + TbJsonToStringDirective, + JsonObjectEditDialogComponent ], imports: [ CommonModule, @@ -357,8 +364,10 @@ import { LedLightComponent } from '@shared/components/led-light.component'; EnumToArrayPipe, HighlightPipe, TruncatePipe, + TbJsonPipe, KeyboardShortcutPipe, - TranslateModule + TranslateModule, + JsonObjectEditDialogComponent ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bd19625619..978e2cda87 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -626,6 +626,7 @@ "details": { "details": "Details", "edit-mode": "Edit mode", + "edit-json": "Edit JSON", "toggle-edit-mode": "Toggle edit mode" }, "device": { @@ -1298,7 +1299,8 @@ "js-func": { "no-return-error": "Function must return value!", "return-type-mismatch": "Function must return value of '{{type}}' type!", - "tidy": "Tidy" + "tidy": "Tidy", + "mini": "Mini" }, "key-val": { "key": "Key", @@ -1635,7 +1637,11 @@ "boolean-value": "Boolean value", "false": "False", "true": "True", - "long": "Long" + "long": "Long", + "json": "JSON", + "json-value": "JSON value", + "json-value-invalid": "JSON value has an invalid format", + "json-value-required": "JSON value is required." }, "widget": { "widget-library": "Widgets Library", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index 02bea6e453..6337b14854 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -607,6 +607,7 @@ }, "details": { "edit-mode": "Режим редактирования", + "edit-json": "Редактировать JSON", "toggle-edit-mode": "Режим редактирования" }, "device": { @@ -1191,8 +1192,7 @@ }, "js-func": { "no-return-error": "Функция должна возвращать значение!", - "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!", - "tidy": "Tidy" + "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!" }, "key-val": { "key": "Ключ", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index d841880dd1..471031c9a3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -724,6 +724,7 @@ "details": { "details": "Деталі", "edit-mode": "Режим редагування", + "edit-json": "Редагувати JSON", "toggle-edit-mode": "Перемкнути режим редагування" }, "device": { @@ -1606,8 +1607,7 @@ }, "js-func": { "no-return-error": "Функція повинна повертати значення!", - "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!", - "tidy": "Tidy" + "return-type-mismatch": "Функція повинна повернути значення типу '{{type}}'!" }, "key-val": { "key": "Ключ",