Browse Source

Create ui to support attribute type JSON (#2471)

pull/2486/head
Vladyslav 6 years ago
committed by GitHub
parent
commit
516ae3d814
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  2. 57
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html
  3. 63
      ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.ts
  4. 96
      ui-ngx/src/app/shared/components/directives/tb-json-to-string.directive.ts
  5. 6
      ui-ngx/src/app/shared/components/json-content.component.html
  6. 8
      ui-ngx/src/app/shared/components/json-content.component.ts
  7. 10
      ui-ngx/src/app/shared/components/json-object-edit.component.html
  8. 19
      ui-ngx/src/app/shared/components/json-object-edit.component.scss
  9. 16
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  10. 16
      ui-ngx/src/app/shared/components/value-input.component.html
  11. 34
      ui-ngx/src/app/shared/components/value-input.component.ts
  12. 10
      ui-ngx/src/app/shared/models/constants.ts
  13. 30
      ui-ngx/src/app/shared/pipe/tbJson.pipe.ts
  14. 13
      ui-ngx/src/app/shared/shared.module.ts
  15. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  16. 4
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  17. 4
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json

2
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -168,7 +168,7 @@
class="tb-value-cell"
(click)="editAttribute($event, attribute)">
<div fxLayout="row">
<span fxFlex>{{attribute.value}}</span>
<span fxFlex>{{attribute.value | tbJson}}</span>
<span [fxShow]="!isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon>
</span>

57
ui-ngx/src/app/shared/components/dialog/json-object-edit-dialog.component.html

@ -0,0 +1,57 @@
<!--
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.
-->
<form [formGroup]="jsonFormGroup" (ngSubmit)="add()" style="min-width: 400px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ (this.data.title ? this.data.title : 'details.edit-json') | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<tb-json-object-edit
formControlName="json"
label="{{ 'value.json-value' | translate }}"
validateContent="true"
[required]="true"
[fillHeight]="false">
</tb-json-object-edit>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || jsonFormGroup.invalid || !jsonFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

63
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<JsonObjectEditDialogComponent, Object>
implements OnInit {
jsonFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: JsonObjectEdittDialogData,
public dialogRef: MatDialogRef<JsonObjectEditDialogComponent, Object>,
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);
}
}

96
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 {
}
}

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

@ -22,9 +22,13 @@
<label class="tb-title no-padding">{{ label }}</label>
<span fxFlex></span>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()">
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJSON()">
{{'js-func.tidy' | translate }}
</button>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="minifyJSON()">
{{'js-func.mini' | translate }}
</button>
<button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen"
class="tb-mat-32"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"

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

@ -257,12 +257,18 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
}
}
beautifyJson() {
beautifyJSON() {
const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60});
this.jsonEditor.setValue(res ? res : '', -1);
this.updateView();
}
minifyJSON() {
const res = JSON.stringify(this.contentBody);
this.jsonEditor.setValue(res ? res : '', -1);
this.updateView();
}
onFullscreen() {
if (this.jsonEditor) {
setTimeout(() => {

10
ui-ngx/src/app/shared/components/json-object-edit.component.html

@ -18,12 +18,20 @@
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="row" fxLayoutAlign="start center" class="tb-json-object-toolbar">
<label class="tb-title no-padding"
ng-class="{'tb-required': required,
'tb-readonly': readonly,
'tb-error': !objectValid}">{{ label }}</label>
<span fxFlex></span>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJSON()">
{{'js-func.tidy' | translate }}
</button>
<button type="button"
mat-button *ngIf="!readonly" class="tidy" (click)="minifyJSON()">
{{'js-func.mini' | translate }}
</button>
<button mat-button mat-icon-button (click)="fullscreen = !fullscreen"
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">

19
ui-ngx/src/app/shared/components/json-object-edit.component.scss

@ -21,6 +21,25 @@
}
}
.tb-json-object-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-object-panel {
height: 100%;
margin-left: 15px;

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

@ -195,6 +195,22 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
}
}
beautifyJSON() {
const res = JSON.stringify(this.modelValue, null, 2);
if (this.jsonEditor) {
this.jsonEditor.setValue(res ? res : '', -1);
}
this.updateView();
}
minifyJSON() {
const res = JSON.stringify(this.modelValue);
if (this.jsonEditor) {
this.jsonEditor.setValue(res ? res : '', -1);
}
this.updateView();
}
writeValue(value: any): void {
this.modelValue = value;
this.contentValue = '';

16
ui-ngx/src/app/shared/components/value-input.component.html

@ -59,5 +59,21 @@
{{ (modelValue ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
</div>
<div fxLayout="row" fxLayoutAlign="center" fxFlex="60" *ngIf="valueType === valueTypeEnum.JSON" class="mat-block">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>value.json-value</mat-label>
<input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel"
[(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"/>
<button matSuffix mat-button mat-icon-button (click)="openEditJSONDialog($event)">
<mat-icon>open_in_new</mat-icon>
</button>
<mat-error *ngIf="value.hasError('required')">
{{ (requiredText ? requiredText : 'value.json-value-required') | translate }}
</mat-error>
<mat-error *ngIf="value.hasError('invalidJSON')">
{{ 'value.json-value-invalid' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</form>

34
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, JsonObjectEdittDialogData, Object>(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;
}

10
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<ValueType, ValueTypeData>(
@ -153,6 +154,13 @@ export const valueTypesMap = new Map<ValueType, ValueTypeData>(
name: 'value.boolean',
icon: 'mdi:checkbox-marked-outline'
}
],
[
ValueType.JSON,
{
name: 'value.json',
icon: 'mdi:json'
}
]
]
);

30
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;
}
}

13
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 { }

10
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",

4
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": "Ключ",

4
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": "Ключ",

Loading…
Cancel
Save