Commit c317be2a65773932663038c5a770edfb0531d991

Authored by Igor Kulikov
1 parent f2a90f18

Implement React Schema Form.

Showing 34 changed files with 1151 additions and 360 deletions
@@ -3767,9 +3767,9 @@ @@ -3767,9 +3767,9 @@
3767 } 3767 }
3768 }, 3768 },
3769 "date-fns": { 3769 "date-fns": {
3770 - "version": "2.5.0",  
3771 - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.5.0.tgz",  
3772 - "integrity": "sha512-I6Tkis01//nRcmvMQw/MRE1HAtcuA5Ie6jGPb8bJZJub7494LGOObqkV3ParnsSVviAjk5C8mNKDqYVBzCopWg==" 3770 + "version": "2.1.0",
  3771 + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.1.0.tgz",
  3772 + "integrity": "sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA=="
3773 }, 3773 },
3774 "date-format": { 3774 "date-format": {
3775 "version": "2.1.0", 3775 "version": "2.1.0",
@@ -4164,9 +4164,9 @@ @@ -4164,9 +4164,9 @@
4164 "dev": true 4164 "dev": true
4165 }, 4165 },
4166 "electron-to-chromium": { 4166 "electron-to-chromium": {
4167 - "version": "1.3.284",  
4168 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.284.tgz",  
4169 - "integrity": "sha512-duOA4IWKH4R8ttiE8q/7xfg6eheRvMKlGqOOcGlDukdHEDJ26Wf7cMrCiK9Am11mswR6E/a23jXVA4UPDthTIw==", 4167 + "version": "1.3.285",
  4168 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.285.tgz",
  4169 + "integrity": "sha512-DYR9KW723sUbGK++DCmCmM95AbNXT4Q0tlCFMcYijFjayhuDqlGYR68OemlP8MJj0gjkwdeItIUfd0oLCgw+4A==",
4170 "dev": true 4170 "dev": true
4171 }, 4171 },
4172 "elliptic": { 4172 "elliptic": {
@@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
41 "base64-js": "^1.3.1", 41 "base64-js": "^1.3.1",
42 "compass-sass-mixins": "^0.12.7", 42 "compass-sass-mixins": "^0.12.7",
43 "core-js": "^3.1.4", 43 "core-js": "^3.1.4",
44 - "date-fns": "^2.5.0", 44 + "date-fns": "2.1.0",
45 "deep-equal": "^1.0.1", 45 "deep-equal": "^1.0.1",
46 "flot": "git://github.com/thingsboard/flot.git#0.9-work", 46 "flot": "git://github.com/thingsboard/flot.git#0.9-work",
47 "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", 47 "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 #entityAliasInput 21 #entityAliasInput
22 formControlName="entityAlias" 22 formControlName="entityAlias"
23 (focusin)="onFocus()" 23 (focusin)="onFocus()"
24 - [required]="required" 24 + [required]="tbRequired"
25 (keydown)="entityAliasEnter($event)" 25 (keydown)="entityAliasEnter($event)"
26 (keypress)="entityAliasEnter($event)" 26 (keypress)="entityAliasEnter($event)"
27 [matAutocomplete]="entityAliasAutocomplete"> 27 [matAutocomplete]="entityAliasAutocomplete">
@@ -54,7 +54,7 @@ @@ -54,7 +54,7 @@
54 </div> 54 </div>
55 </mat-option> 55 </mat-option>
56 </mat-autocomplete> 56 </mat-autocomplete>
57 - <mat-error *ngIf="!modelValue && required"> 57 + <mat-error *ngIf="!modelValue && tbRequired">
58 {{ 'entity.alias-required' | translate }} 58 {{ 'entity.alias-required' | translate }}
59 </mat-error> 59 </mat-error>
60 </mat-form-field> 60 </mat-form-field>
@@ -75,11 +75,11 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, @@ -75,11 +75,11 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
75 75
76 76
77 private requiredValue: boolean; 77 private requiredValue: boolean;
78 - get required(): boolean { 78 + get tbRequired(): boolean {
79 return this.requiredValue; 79 return this.requiredValue;
80 } 80 }
81 @Input() 81 @Input()
82 - set required(value: boolean) { 82 + set tbRequired(value: boolean) {
83 this.requiredValue = coerceBooleanProperty(value); 83 this.requiredValue = coerceBooleanProperty(value);
84 } 84 }
85 85
@@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, @@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
151 151
152 isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { 152 isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
153 const originalErrorState = this.errorStateMatcher.isErrorState(control, form); 153 const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
154 - const customErrorState = this.required && !this.modelValue; 154 + const customErrorState = this.tbRequired && !this.modelValue;
155 return originalErrorState || customErrorState; 155 return originalErrorState || customErrorState;
156 } 156 }
157 157
@@ -16,9 +16,8 @@ @@ -16,9 +16,8 @@
16 16
17 --> 17 -->
18 <mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}" 18 <mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}"
19 - [formGroup]="dataKeyFormGroup"  
20 class="tb-datakey-config"> 19 class="tb-datakey-config">
21 - <mat-tab label="{{ 'datakey.settings' | translate }}"> 20 + <mat-tab [formGroup]="dataKeyFormGroup" label="{{ 'datakey.settings' | translate }}">
22 <div class="mat-content mat-padding" fxLayout="column"> 21 <div class="mat-content mat-padding" fxLayout="column">
23 <mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function"> 22 <mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function">
24 <mat-label>{{ 'entity.key' | translate }}</mat-label> 23 <mat-label>{{ 'entity.key' | translate }}</mat-label>
@@ -94,12 +93,10 @@ @@ -94,12 +93,10 @@
94 </section> 93 </section>
95 </div> 94 </div>
96 </mat-tab> 95 </mat-tab>
97 - <mat-tab label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced"> 96 + <mat-tab [formGroup]="dataKeySettingsFormGroup" label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced">
98 <div class="mat-content mat-padding" fxLayout="column"> 97 <div class="mat-content mat-padding" fxLayout="column">
99 <div style="overflow: auto;"> 98 <div style="overflow: auto;">
100 <tb-json-form 99 <tb-json-form
101 - [schema]="dataKeySchema"  
102 - [form]="dataKeyForm"  
103 formControlName="settings"> 100 formControlName="settings">
104 </tb-json-form> 101 </tb-json-form>
105 </div> 102 </div>
@@ -39,6 +39,7 @@ import { Observable, of } from 'rxjs'; @@ -39,6 +39,7 @@ import { Observable, of } from 'rxjs';
39 import { map, mergeMap, tap } from 'rxjs/operators'; 39 import { map, mergeMap, tap } from 'rxjs/operators';
40 import { alarmFields } from '@shared/models/alarm.models'; 40 import { alarmFields } from '@shared/models/alarm.models';
41 import { JsFuncComponent } from '@shared/components/js-func.component'; 41 import { JsFuncComponent } from '@shared/components/js-func.component';
  42 +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
42 43
43 @Component({ 44 @Component({
44 selector: 'tb-data-key-config', 45 selector: 'tb-data-key-config',
@@ -77,15 +78,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -77,15 +78,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
77 78
78 displayAdvanced = false; 79 displayAdvanced = false;
79 80
80 - dataKeySchema: any;  
81 - dataKeyForm: any;  
82 -  
83 private modelValue: DataKey; 81 private modelValue: DataKey;
84 82
85 private propagateChange = null; 83 private propagateChange = null;
86 84
87 public dataKeyFormGroup: FormGroup; 85 public dataKeyFormGroup: FormGroup;
88 86
  87 + public dataKeySettingsFormGroup: FormGroup;
  88 +
  89 + private dataKeySettingsData: JsonFormComponentData;
  90 +
89 private alarmKeys: Array<DataKey>; 91 private alarmKeys: Array<DataKey>;
90 92
91 filteredKeys: Observable<Array<string>>; 93 filteredKeys: Observable<Array<string>>;
@@ -112,8 +114,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -112,8 +114,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
112 } 114 }
113 if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { 115 if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) {
114 this.displayAdvanced = true; 116 this.displayAdvanced = true;
115 - this.dataKeySchema = this.dataKeySettingsSchema.schema;  
116 - this.dataKeyForm = this.dataKeySettingsSchema.form || ['*']; 117 + this.dataKeySettingsData = {
  118 + schema: this.dataKeySettingsSchema.schema,
  119 + form: this.dataKeySettingsSchema.form || ['*']
  120 + };
  121 + this.dataKeySettingsFormGroup = this.fb.group({
  122 + settings: [null, []]
  123 + });
  124 + this.dataKeySettingsFormGroup.valueChanges.subscribe(() => {
  125 + this.updateModel();
  126 + });
117 } 127 }
118 this.dataKeyFormGroup = this.fb.group({ 128 this.dataKeyFormGroup = this.fb.group({
119 name: [null, []], 129 name: [null, []],
@@ -123,8 +133,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -123,8 +133,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
123 decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], 133 decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
124 funcBody: [null, []], 134 funcBody: [null, []],
125 usePostProcessing: [null, []], 135 usePostProcessing: [null, []],
126 - postFuncBody: [null, []],  
127 - settings: [null, []] 136 + postFuncBody: [null, []]
128 }); 137 });
129 138
130 this.dataKeyFormGroup.valueChanges.subscribe(() => { 139 this.dataKeyFormGroup.valueChanges.subscribe(() => {
@@ -162,10 +171,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -162,10 +171,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
162 this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); 171 this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
163 this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []); 172 this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []);
164 this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); 173 this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
  174 + if (this.displayAdvanced) {
  175 + this.dataKeySettingsData.model = this.modelValue.settings;
  176 + this.dataKeySettingsFormGroup.patchValue({
  177 + settings: this.dataKeySettingsData
  178 + }, {emitEvent: false});
  179 + }
165 } 180 }
166 181
167 private updateModel() { 182 private updateModel() {
168 this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; 183 this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value};
  184 + if (this.displayAdvanced) {
  185 + this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model;
  186 + }
169 this.propagateChange(this.modelValue); 187 this.propagateChange(this.modelValue);
170 } 188 }
171 189
@@ -222,6 +240,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @@ -222,6 +240,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
222 } 240 }
223 }; 241 };
224 } 242 }
  243 + if (this.displayAdvanced && !this.dataKeySettingsFormGroup.valid) {
  244 + return {
  245 + dataKeySettings: {
  246 + valid: false
  247 + }
  248 + };
  249 + }
225 return null; 250 return null;
226 } 251 }
227 } 252 }
@@ -14,15 +14,6 @@ @@ -14,15 +14,6 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
17 -@mixin tb-checkered-bg() {  
18 - background-color: #fff;  
19 - background-image:  
20 - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),  
21 - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);  
22 - background-position: 0 0, 4px 4px;  
23 - background-size: 8px 8px;  
24 -}  
25 -  
26 :host { 17 :host {
27 .mat-chip.mat-standard-chip { 18 .mat-chip.mat-standard-chip {
28 .tb-attribute-chip { 19 .tb-attribute-chip {
@@ -56,27 +47,6 @@ @@ -56,27 +47,6 @@
56 color: inherit; 47 color: inherit;
57 opacity: inherit; 48 opacity: inherit;
58 } 49 }
59 -  
60 - .tb-color-preview {  
61 - cursor: pointer;  
62 - box-sizing: border-box;  
63 - position: relative;  
64 - width: 24px;  
65 - min-width: 24px;  
66 - height: 24px;  
67 - overflow: hidden;  
68 - content: "";  
69 - border: 2px solid #fff;  
70 - border-radius: 50%;  
71 - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084);  
72 -  
73 - @include tb-checkered-bg();  
74 -  
75 - .tb-color-result {  
76 - width: 100%;  
77 - height: 100%;  
78 - }  
79 - }  
80 } 50 }
81 } 51 }
82 } 52 }
@@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
31 </mat-checkbox> 31 </mat-checkbox>
32 </div> 32 </div>
33 <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> 33 <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
34 - <span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span> 34 + <span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate style="padding-right: 8px;">widget-config.timewindow</span>
35 <tb-timewindow asButton="true" 35 <tb-timewindow asButton="true"
36 aggregation="{{ widgetType === widgetTypes.timeseries }}" 36 aggregation="{{ widgetType === widgetTypes.timeseries }}"
37 fxFlex formControlName="timewindow"></tb-timewindow> 37 fxFlex formControlName="timewindow"></tb-timewindow>
@@ -64,12 +64,12 @@ @@ -64,12 +64,12 @@
64 <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc && 64 <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
65 widgetType !== widgetTypes.alarm && 65 widgetType !== widgetTypes.alarm &&
66 widgetType !== widgetTypes.static && 66 widgetType !== widgetTypes.static &&
67 - isDataEnabled" [expanded]="true"> 67 + modelValue?.isDataEnabled" [expanded]="true">
68 <mat-expansion-panel-header> 68 <mat-expansion-panel-header>
69 <mat-panel-title fxLayout="column"> 69 <mat-panel-title fxLayout="column">
70 <div class="tb-panel-title" translate>widget-config.datasources</div> 70 <div class="tb-panel-title" translate>widget-config.datasources</div>
71 - <div *ngIf="typeParameters && typeParameters.maxDatasources > -1"  
72 - class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: typeParameters.maxDatasources} }}</div> 71 + <div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
  72 + class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
73 </mat-panel-title> 73 </mat-panel-title>
74 </mat-expansion-panel-header> 74 </mat-expansion-panel-header>
75 <div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate"> 75 <div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate">
@@ -90,7 +90,7 @@ @@ -90,7 +90,7 @@
90 <div style="overflow: auto; padding-bottom: 15px;"> 90 <div style="overflow: auto; padding-bottom: 15px;">
91 <div fxFlex fxLayout="row" fxLayoutAlign="start center" 91 <div fxFlex fxLayout="row" fxLayoutAlign="start center"
92 formArrayName="datasources" 92 formArrayName="datasources"
93 - *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index"> 93 + *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index;">
94 <span fxFlex="5">{{$index + 1}}.</span> 94 <span fxFlex="5">{{$index + 1}}.</span>
95 <div [formGroupName]="$index" class="mat-elevation-z4" fxFlex 95 <div [formGroupName]="$index" class="mat-elevation-z4" fxFlex
96 fxLayout="row" 96 fxLayout="row"
@@ -120,7 +120,7 @@ @@ -120,7 +120,7 @@
120 </ng-template> 120 </ng-template>
121 <ng-template [ngSwitchCase]="datasourceType.entity"> 121 <ng-template [ngSwitchCase]="datasourceType.entity">
122 <tb-entity-alias-select 122 <tb-entity-alias-select
123 - [required]="datasourceControl.get('type').value === datasourceType.entity" 123 + [tbRequired]="datasourceControl.get('type').value === datasourceType.entity"
124 [aliasController]="aliasController" 124 [aliasController]="aliasController"
125 formControlName="entityAliasId" 125 formControlName="entityAliasId"
126 [callbacks]="widgetConfigCallbacks"> 126 [callbacks]="widgetConfigCallbacks">
@@ -130,25 +130,15 @@ @@ -130,25 +130,15 @@
130 <tb-data-keys class="tb-data-keys" fxFlex 130 <tb-data-keys class="tb-data-keys" fxFlex
131 [widgetType]="widgetType" 131 [widgetType]="widgetType"
132 [datasourceType]="datasourceControl.get('type').value" 132 [datasourceType]="datasourceControl.get('type').value"
133 - [maxDataKeys]="typeParameters?.maxDataKeys"  
134 - [optDataKeys]="typeParameters?.dataKeysOptional" 133 + [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
  134 + [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
135 [aliasController]="aliasController" 135 [aliasController]="aliasController"
136 - [datakeySettingsSchema]="dataKeySettingsSchema" 136 + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
137 [callbacks]="widgetConfigCallbacks" 137 [callbacks]="widgetConfigCallbacks"
138 [entityAliasId]="datasourceControl.get('entityAliasId').value" 138 [entityAliasId]="datasourceControl.get('entityAliasId').value"
139 formControlName="dataKeys"> 139 formControlName="dataKeys">
140 </tb-data-keys> 140 </tb-data-keys>
141 </section> 141 </section>
142 - <!--tb-datasource fxFlex  
143 - [widgetType]="widgetType"  
144 - [maxDataKeys]="typeParameters?.maxDataKeys"  
145 - [optDataKeys]="typeParameters?.dataKeysOptional"  
146 - [aliasController]="aliasController"  
147 - [functionsOnly]="functionsOnly"  
148 - [datakeySettingsSchema]="dataKeySettingsSchema"  
149 - [callbacks]="widgetConfigCallbacks"  
150 - [formControl]="datasourceControl"  
151 - ></tb-datasource-->  
152 <button [disabled]="isLoading$ | async" 142 <button [disabled]="isLoading$ | async"
153 mat-button mat-icon-button color="primary" 143 mat-button mat-icon-button color="primary"
154 style="min-width: 40px;" 144 style="min-width: 40px;"
@@ -164,8 +154,8 @@ @@ -164,8 +154,8 @@
164 <div fxFlex fxLayout="row" fxLayoutAlign="start center"> 154 <div fxFlex fxLayout="row" fxLayoutAlign="start center">
165 <button [disabled]="isLoading$ | async" 155 <button [disabled]="isLoading$ | async"
166 mat-button mat-raised-button color="primary" 156 mat-button mat-raised-button color="primary"
167 - [fxShow]="typeParameters &&  
168 - (typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < typeParameters.maxDatasources)" 157 + [fxShow]="modelValue?.typeParameters &&
  158 + (modelValue?.typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < modelValue?.typeParameters.maxDatasources)"
169 (click)="addDatasource()" 159 (click)="addDatasource()"
170 matTooltip="{{ 'widget-config.add-datasource' | translate }}" 160 matTooltip="{{ 'widget-config.add-datasource' | translate }}"
171 matTooltipPosition="above"> 161 matTooltipPosition="above">
@@ -175,7 +165,7 @@ @@ -175,7 +165,7 @@
175 </div> 165 </div>
176 </mat-expansion-panel> 166 </mat-expansion-panel>
177 <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc && 167 <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
178 - isDataEnabled" [expanded]="true"> 168 + modelValue?.isDataEnabled" [expanded]="true">
179 <mat-expansion-panel-header> 169 <mat-expansion-panel-header>
180 <mat-panel-title> 170 <mat-panel-title>
181 {{ 'widget-config.target-device' | translate }} 171 {{ 'widget-config.target-device' | translate }}
@@ -183,7 +173,7 @@ @@ -183,7 +173,7 @@
183 </mat-expansion-panel-header> 173 </mat-expansion-panel-header>
184 <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;"> 174 <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
185 <tb-entity-alias-select fxFlex 175 <tb-entity-alias-select fxFlex
186 - [required]="!widgetEditMode" 176 + [tbRequired]="!widgetEditMode"
187 [aliasController]="aliasController" 177 [aliasController]="aliasController"
188 [allowedEntityTypes]="[entityTypes.DEVICE]" 178 [allowedEntityTypes]="[entityTypes.DEVICE]"
189 [callbacks]="widgetConfigCallbacks" 179 [callbacks]="widgetConfigCallbacks"
@@ -193,6 +183,14 @@ @@ -193,6 +183,14 @@
193 </mat-expansion-panel> 183 </mat-expansion-panel>
194 </div> 184 </div>
195 </mat-tab> 185 </mat-tab>
196 - <mat-tab label="{{ 'widget-config.settings' | translate }}"> 186 + <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
  187 + <div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config"
  188 + fxLayout="column">
  189 + <tb-json-form
  190 + formControlName="settings">
  191 + </tb-json-form>
  192 + </div>
  193 + </mat-tab>
  194 + <mat-tab label="{{ 'widget-config.actions' | translate }}">
197 </mat-tab> 195 </mat-tab>
198 </mat-tab-group> 196 </mat-tab-group>
@@ -25,7 +25,6 @@ import { @@ -25,7 +25,6 @@ import {
25 LegendConfig, 25 LegendConfig,
26 WidgetActionDescriptor, 26 WidgetActionDescriptor,
27 WidgetActionSource, 27 WidgetActionSource,
28 - WidgetConfigSettings,  
29 widgetType, 28 widgetType,
30 WidgetTypeParameters 29 WidgetTypeParameters
31 } from '@shared/models/widget.models'; 30 } from '@shared/models/widget.models';
@@ -38,7 +37,7 @@ import { @@ -38,7 +37,7 @@ import {
38 FormGroup, 37 FormGroup,
39 NG_VALIDATORS, 38 NG_VALIDATORS,
40 NG_VALUE_ACCESSOR, 39 NG_VALUE_ACCESSOR,
41 - Validator, 40 + Validator, ValidatorFn,
42 Validators 41 Validators
43 } from '@angular/forms'; 42 } from '@angular/forms';
44 import { WidgetConfigComponentData } from '@home/models/widget-component.models'; 43 import { WidgetConfigComponentData } from '@home/models/widget-component.models';
@@ -59,6 +58,16 @@ import { @@ -59,6 +58,16 @@ import {
59 import { tap, mergeMap, map, catchError } from 'rxjs/operators'; 58 import { tap, mergeMap, map, catchError } from 'rxjs/operators';
60 import { MatDialog } from '@angular/material/dialog'; 59 import { MatDialog } from '@angular/material/dialog';
61 import { EntityService } from '@core/http/entity.service'; 60 import { EntityService } from '@core/http/entity.service';
  61 +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
  62 +
  63 +const emptySettingsSchema = {
  64 + type: 'object',
  65 + properties: {}
  66 +};
  67 +const emptySettingsGroupInfoes = [];
  68 +const defaultSettingsForm = [
  69 + '*'
  70 +];
62 71
63 @Component({ 72 @Component({
64 selector: 'tb-widget-config', 73 selector: 'tb-widget-config',
@@ -89,27 +98,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -89,27 +98,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
89 forceExpandDatasources: boolean; 98 forceExpandDatasources: boolean;
90 99
91 @Input() 100 @Input()
92 - isDataEnabled: boolean;  
93 -  
94 - @Input()  
95 - typeParameters: WidgetTypeParameters;  
96 -  
97 - @Input()  
98 - actionSources: {[key: string]: WidgetActionSource};  
99 -  
100 - @Input()  
101 aliasController: IAliasController; 101 aliasController: IAliasController;
102 102
103 @Input() 103 @Input()
104 entityAliases: EntityAliases; 104 entityAliases: EntityAliases;
105 105
106 @Input() 106 @Input()
107 - widgetSettingsSchema: any;  
108 -  
109 - @Input()  
110 - dataKeySettingsSchema: any;  
111 -  
112 - @Input()  
113 functionsOnly: boolean; 107 functionsOnly: boolean;
114 108
115 @Input() disabled: boolean; 109 @Input() disabled: boolean;
@@ -149,37 +143,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -149,37 +143,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
149 legendConfig: LegendConfig; 143 legendConfig: LegendConfig;
150 actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; 144 actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
151 alarmSource: Datasource; 145 alarmSource: Datasource;
152 - settings: WidgetConfigSettings;  
153 mobileOrder: number; 146 mobileOrder: number;
154 mobileHeight: number; 147 mobileHeight: number;
155 148
156 - emptySettingsSchema = {  
157 - type: 'object',  
158 - properties: {}  
159 - };  
160 -  
161 - emptySettingsGroupInfoes = [];  
162 -  
163 - defaultSettingsForm = [  
164 - '*'  
165 - ];  
166 -  
167 - currentSettingsSchema = deepClone(this.emptySettingsSchema);  
168 -  
169 - currentSettings: WidgetConfigSettings = {};  
170 - currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);  
171 -  
172 - currentSettingsForm: any;  
173 -  
174 private modelValue: WidgetConfigComponentData; 149 private modelValue: WidgetConfigComponentData;
175 150
176 private propagateChange = null; 151 private propagateChange = null;
177 152
178 public dataSettings: FormGroup; 153 public dataSettings: FormGroup;
179 public targetDeviceSettings: FormGroup; 154 public targetDeviceSettings: FormGroup;
  155 + public advancedSettings: FormGroup;
180 156
181 private dataSettingsChangesSubscription: Subscription; 157 private dataSettingsChangesSubscription: Subscription;
182 private targetDeviceSettingsSubscription: Subscription; 158 private targetDeviceSettingsSubscription: Subscription;
  159 + private advancedSettingsSubscription: Subscription;
183 160
184 constructor(protected store: Store<AppState>, 161 constructor(protected store: Store<AppState>,
185 private utils: UtilsService, 162 private utils: UtilsService,
@@ -207,6 +184,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -207,6 +184,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
207 this.targetDeviceSettingsSubscription.unsubscribe(); 184 this.targetDeviceSettingsSubscription.unsubscribe();
208 this.targetDeviceSettingsSubscription = null; 185 this.targetDeviceSettingsSubscription = null;
209 } 186 }
  187 + if (this.advancedSettingsSubscription) {
  188 + this.advancedSettingsSubscription.unsubscribe();
  189 + this.advancedSettingsSubscription = null;
  190 + }
210 } 191 }
211 192
212 private createChangeSubscriptions() { 193 private createChangeSubscriptions() {
@@ -216,11 +197,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -216,11 +197,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
216 this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( 197 this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe(
217 () => this.updateTargetDeviceSettings() 198 () => this.updateTargetDeviceSettings()
218 ); 199 );
  200 + this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe(
  201 + () => this.updateAdvancedSettings()
  202 + );
219 } 203 }
220 204
221 private buildForms() { 205 private buildForms() {
222 this.dataSettings = this.fb.group({}); 206 this.dataSettings = this.fb.group({});
223 this.targetDeviceSettings = this.fb.group({}); 207 this.targetDeviceSettings = this.fb.group({});
  208 + this.advancedSettings = this.fb.group({});
224 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { 209 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
225 this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); 210 this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
226 this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); 211 this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
@@ -240,7 +225,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -240,7 +225,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
240 [Validators.required, Validators.min(1)])); 225 [Validators.required, Validators.min(1)]));
241 } 226 }
242 } 227 }
243 - if (this.isDataEnabled) { 228 + if (this.modelValue.isDataEnabled) {
244 if (this.widgetType !== widgetType.rpc && 229 if (this.widgetType !== widgetType.rpc &&
245 this.widgetType !== widgetType.alarm && 230 this.widgetType !== widgetType.alarm &&
246 this.widgetType !== widgetType.static) { 231 this.widgetType !== widgetType.static) {
@@ -252,6 +237,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -252,6 +237,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
252 this.widgetEditMode ? [] : [Validators.required])); 237 this.widgetEditMode ? [] : [Validators.required]));
253 } 238 }
254 } 239 }
  240 + this.advancedSettings.addControl('settings',
  241 + this.fb.control(null, []));
255 } 242 }
256 243
257 registerOnChange(fn: any): void { 244 registerOnChange(fn: any): void {
@@ -323,7 +310,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -323,7 +310,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
323 { timewindow: config.timewindow }, {emitEvent: false} 310 { timewindow: config.timewindow }, {emitEvent: false}
324 ); 311 );
325 } 312 }
326 - if (this.isDataEnabled) { 313 + if (this.modelValue.isDataEnabled) {
327 if (this.widgetType !== widgetType.rpc && 314 if (this.widgetType !== widgetType.rpc &&
328 this.widgetType !== widgetType.alarm && 315 this.widgetType !== widgetType.alarm &&
329 this.widgetType !== widgetType.static) { 316 this.widgetType !== widgetType.static) {
@@ -366,9 +353,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -366,9 +353,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
366 } 353 }
367 } 354 }
368 } 355 }
369 - this.settings = config.settings;  
370 356
371 - this.updateSchemaForm(); 357 + this.updateSchemaForm(config.settings);
372 358
373 if (layout) { 359 if (layout) {
374 this.mobileOrder = layout.mobileOrder; 360 this.mobileOrder = layout.mobileOrder;
@@ -383,7 +369,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -383,7 +369,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
383 } 369 }
384 370
385 private buildDatasourceForm(datasource?: Datasource): AbstractControl { 371 private buildDatasourceForm(datasource?: Datasource): AbstractControl {
386 - const dataKeysRequired = !this.typeParameters || !this.typeParameters.dataKeysOptional; 372 + const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional;
387 const datasourceFormGroup = this.fb.group( 373 const datasourceFormGroup = this.fb.group(
388 { 374 {
389 type: [datasource ? datasource.type : null, [Validators.required]], 375 type: [datasource ? datasource.type : null, [Validators.required]],
@@ -402,18 +388,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -402,18 +388,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
402 return datasourceFormGroup; 388 return datasourceFormGroup;
403 } 389 }
404 390
405 - private updateSchemaForm() {  
406 - if (this.widgetSettingsSchema && this.widgetSettingsSchema.schema) {  
407 - this.currentSettingsSchema = this.widgetSettingsSchema.schema;  
408 - this.currentSettingsForm = this.widgetSettingsSchema.form || deepClone(this.defaultSettingsForm);  
409 - this.currentSettingsGroupInfoes = this.widgetSettingsSchema.groupInfoes;  
410 - this.currentSettings = this.settings; 391 + private updateSchemaForm(settings?: any) {
  392 + const widgetSettingsFormData: JsonFormComponentData = {};
  393 + if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {
  394 + widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema;
  395 + widgetSettingsFormData.form = this.modelValue.settingsSchema.form || deepClone(defaultSettingsForm);
  396 + widgetSettingsFormData.groupInfoes = this.modelValue.settingsSchema.groupInfoes;
  397 + widgetSettingsFormData.model = settings;
411 } else { 398 } else {
412 - this.currentSettingsForm = deepClone(this.defaultSettingsForm);  
413 - this.currentSettingsSchema = deepClone(this.emptySettingsSchema);  
414 - this.currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);  
415 - this.currentSettings = {}; 399 + widgetSettingsFormData.schema = deepClone(emptySettingsSchema);
  400 + widgetSettingsFormData.form = deepClone(defaultSettingsForm);
  401 + widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes);
  402 + widgetSettingsFormData.model = {};
416 } 403 }
  404 + this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
417 } 405 }
418 406
419 private updateDataSettings() { 407 private updateDataSettings() {
@@ -439,8 +427,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -439,8 +427,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
439 } 427 }
440 } 428 }
441 429
  430 + private updateAdvancedSettings() {
  431 + if (this.modelValue) {
  432 + if (this.modelValue.config) {
  433 + const settings = this.advancedSettings.get('settings').value.model;
  434 + this.modelValue.config.settings = settings;
  435 + }
  436 + this.propagateChange(this.modelValue);
  437 + }
  438 + }
  439 +
442 public displayAdvanced(): boolean { 440 public displayAdvanced(): boolean {
443 - return this.widgetSettingsSchema && this.widgetSettingsSchema.schema; 441 + return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema;
444 } 442 }
445 443
446 public removeDatasource(index: number) { 444 public removeDatasource(index: number) {
@@ -450,7 +448,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -450,7 +448,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
450 public addDatasource() { 448 public addDatasource() {
451 let newDatasource: Datasource; 449 let newDatasource: Datasource;
452 if (this.functionsOnly) { 450 if (this.functionsOnly) {
453 - newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); 451 + newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema));
454 newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)]; 452 newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)];
455 } else { 453 } else {
456 newDatasource = { type: DatasourceType.entity, 454 newDatasource = { type: DatasourceType.entity,
@@ -489,8 +487,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -489,8 +487,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
489 result.funcBody = 'return prevValue + 1;'; 487 result.funcBody = 'return prevValue + 1;';
490 } 488 }
491 } 489 }
492 - if (isDefined(this.dataKeySettingsSchema.schema)) {  
493 - result.settings = this.utils.generateObjectFromJsonSchema(this.dataKeySettingsSchema.schema); 490 + if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) {
  491 + result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema);
494 } 492 }
495 return result; 493 return result;
496 } 494 }
@@ -605,9 +603,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -605,9 +603,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
605 valid: false 603 valid: false
606 } 604 }
607 }; 605 };
  606 + } else if (!this.advancedSettings.valid) {
  607 + return {
  608 + advancedSettings: {
  609 + valid: false
  610 + }
  611 + };
608 } else { 612 } else {
609 const config = this.modelValue.config; 613 const config = this.modelValue.config;
610 - if (this.widgetType === widgetType.rpc && this.isDataEnabled) { 614 + if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) {
611 if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) { 615 if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) {
612 return { 616 return {
613 targetDeviceAliasIds: { 617 targetDeviceAliasIds: {
@@ -615,7 +619,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -615,7 +619,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
615 } 619 }
616 }; 620 };
617 } 621 }
618 - } else if (this.widgetType === widgetType.alarm && this.isDataEnabled) { 622 + } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) {
619 if (!config.alarmSource) { 623 if (!config.alarmSource) {
620 return { 624 return {
621 alarmSource: { 625 alarmSource: {
@@ -623,7 +627,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -623,7 +627,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
623 } 627 }
624 }; 628 };
625 } 629 }
626 - } else if (this.widgetType !== widgetType.static && this.isDataEnabled) { 630 + } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) {
627 if (!config.datasources || !config.datasources.length) { 631 if (!config.datasources || !config.datasources.length) {
628 return { 632 return {
629 datasources: { 633 datasources: {
@@ -22,7 +22,6 @@ import { @@ -22,7 +22,6 @@ import {
22 WidgetActionDescriptor, 22 WidgetActionDescriptor,
23 WidgetActionSource, 23 WidgetActionSource,
24 WidgetConfig, 24 WidgetConfig,
25 - WidgetConfigSettings,  
26 WidgetControllerDescriptor, 25 WidgetControllerDescriptor,
27 WidgetType, 26 WidgetType,
28 widgetType, 27 widgetType,
@@ -74,7 +73,7 @@ export interface WidgetContext { @@ -74,7 +73,7 @@ export interface WidgetContext {
74 isMobile?: boolean; 73 isMobile?: boolean;
75 dashboard?: IDashboardComponent; 74 dashboard?: IDashboardComponent;
76 widgetConfig?: WidgetConfig; 75 widgetConfig?: WidgetConfig;
77 - settings?: WidgetConfigSettings; 76 + settings?: any;
78 units?: string; 77 units?: string;
79 decimals?: number; 78 decimals?: number;
80 subscriptions?: {[id: string]: IWidgetSubscription}; 79 subscriptions?: {[id: string]: IWidgetSubscription};
@@ -122,6 +121,11 @@ export interface WidgetConfigComponentData { @@ -122,6 +121,11 @@ export interface WidgetConfigComponentData {
122 config: WidgetConfig; 121 config: WidgetConfig;
123 layout: WidgetLayout; 122 layout: WidgetLayout;
124 widgetType: widgetType; 123 widgetType: widgetType;
  124 + typeParameters: WidgetTypeParameters;
  125 + actionSources: {[key: string]: WidgetActionSource};
  126 + isDataEnabled: boolean;
  127 + settingsSchema: any;
  128 + dataKeySettingsSchema: any;
125 } 129 }
126 130
127 export const MissingWidgetType: WidgetInfo = { 131 export const MissingWidgetType: WidgetInfo = {
@@ -172,7 +172,7 @@ @@ -172,7 +172,7 @@
172 [opened]="isEditingWidget" 172 [opened]="isEditingWidget"
173 mode="over" 173 mode="over"
174 position="end"> 174 position="end">
175 - <tb-details-panel fxFlex 175 + <tb-details-panel *ngIf="isEditingWidget" fxFlex
176 headerTitle="{{editingWidget?.config.title}}" 176 headerTitle="{{editingWidget?.config.title}}"
177 headerSubtitle="{{ editingWidgetSubtitle }}" 177 headerSubtitle="{{ editingWidgetSubtitle }}"
178 [isReadOnly]="false" 178 [isReadOnly]="false"
@@ -180,20 +180,17 @@ @@ -180,20 +180,17 @@
180 (closeDetails)="onEditWidgetClosed()" 180 (closeDetails)="onEditWidgetClosed()"
181 (toggleDetailsEditMode)="onRevertWidgetEdit()" 181 (toggleDetailsEditMode)="onRevertWidgetEdit()"
182 (applyDetails)="saveWidget()" 182 (applyDetails)="saveWidget()"
183 - [theForm]="widgetForm"> 183 + [theForm]="tbEditWidget.widgetForm">
184 <div class="details-buttons"> 184 <div class="details-buttons">
185 <div [tb-help]="helpLinkIdForWidgetType()"></div> 185 <div [tb-help]="helpLinkIdForWidgetType()"></div>
186 </div> 186 </div>
187 - <form #widgetForm="ngForm" [formGroup]="editingWidgetFormGroup">  
188 - <tb-edit-widget *ngIf="isEditingWidget"  
189 - [dashboard]="dashboard"  
190 - [aliasController]="dashboardCtx.aliasController"  
191 - [widgetEditMode]="widgetEditMode"  
192 - [widget]="editingWidget"  
193 - [widgetLayout]="editingWidgetLayout"  
194 - [widgetFormGroup]="editingWidgetFormGroup">  
195 - </tb-edit-widget>  
196 - </form> 187 + <tb-edit-widget #tbEditWidget
  188 + [dashboard]="dashboard"
  189 + [aliasController]="dashboardCtx.aliasController"
  190 + [widgetEditMode]="widgetEditMode"
  191 + [widget]="editingWidget"
  192 + [widgetLayout]="editingWidgetLayout">
  193 + </tb-edit-widget>
197 </tb-details-panel> 194 </tb-details-panel>
198 </mat-drawer> 195 </mat-drawer>
199 </mat-drawer-container> 196 </mat-drawer-container>
@@ -71,6 +71,7 @@ import { @@ -71,6 +71,7 @@ import {
71 EntityAliasesDialogData 71 EntityAliasesDialogData
72 } from '@home/components/alias/entity-aliases-dialog.component'; 72 } from '@home/components/alias/entity-aliases-dialog.component';
73 import { EntityAliases } from '@app/shared/models/alias.models'; 73 import { EntityAliases } from '@app/shared/models/alias.models';
  74 +import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component';
74 75
75 @Component({ 76 @Component({
76 selector: 'tb-dashboard-page', 77 selector: 'tb-dashboard-page',
@@ -109,7 +110,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -109,7 +110,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
109 editingWidgetLayoutOriginal: WidgetLayout = null; 110 editingWidgetLayoutOriginal: WidgetLayout = null;
110 editingWidgetSubtitle: string = null; 111 editingWidgetSubtitle: string = null;
111 editingLayoutCtx: DashboardPageLayoutContext = null; 112 editingLayoutCtx: DashboardPageLayoutContext = null;
112 - editingWidgetFormGroup: FormGroup;  
113 113
114 thingsboardVersion: string = env.tbVersion; 114 thingsboardVersion: string = env.tbVersion;
115 115
@@ -188,6 +188,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -188,6 +188,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
188 set rightLayoutOpened(rightLayoutOpened: boolean) { 188 set rightLayoutOpened(rightLayoutOpened: boolean) {
189 } 189 }
190 190
  191 + @ViewChild('tbEditWidget', {static: false}) editWidgetComponent: EditWidgetComponent;
  192 +
191 constructor(protected store: Store<AppState>, 193 constructor(protected store: Store<AppState>,
192 @Inject(WINDOW) private window: Window, 194 @Inject(WINDOW) private window: Window,
193 private breakpointObserver: BreakpointObserver, 195 private breakpointObserver: BreakpointObserver,
@@ -205,10 +207,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -205,10 +207,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
205 private dialog: MatDialog) { 207 private dialog: MatDialog) {
206 super(store); 208 super(store);
207 209
208 - this.editingWidgetFormGroup = this.fb.group({  
209 - widgetConfig: [null]  
210 - });  
211 -  
212 this.rxSubscriptions.push(this.route.data.subscribe( 210 this.rxSubscriptions.push(this.route.data.subscribe(
213 (data) => { 211 (data) => {
214 this.init(data); 212 this.init(data);
@@ -630,15 +628,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -630,15 +628,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
630 } 628 }
631 629
632 onRevertWidgetEdit() { 630 onRevertWidgetEdit() {
633 - if (this.editingWidgetFormGroup.dirty) {  
634 - this.editingWidgetFormGroup.markAsPristine(); 631 + if (this.editWidgetComponent.widgetFormGroup.dirty) {
  632 + this.editWidgetComponent.widgetFormGroup.markAsPristine();
635 this.editingWidget = deepClone(this.editingWidgetOriginal); 633 this.editingWidget = deepClone(this.editingWidgetOriginal);
636 this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal); 634 this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
637 } 635 }
638 } 636 }
639 637
640 saveWidget() { 638 saveWidget() {
641 - this.editingWidgetFormGroup.markAsPristine(); 639 + this.editWidgetComponent.widgetFormGroup.markAsPristine();
642 const widget = deepClone(this.editingWidget); 640 const widget = deepClone(this.editingWidget);
643 const widgetLayout = deepClone(this.editingWidgetLayout); 641 const widgetLayout = deepClone(this.editingWidgetLayout);
644 const id = this.editingWidgetOriginal.id; 642 const id = this.editingWidgetOriginal.id;
@@ -15,14 +15,9 @@ @@ -15,14 +15,9 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<form [formGroup]="widgetFormGroup"> 18 +<form #widgetForm="ngForm" [formGroup]="widgetFormGroup">
19 <fieldset [disabled]="isLoading$ | async"> 19 <fieldset [disabled]="isLoading$ | async">
20 <tb-widget-config 20 <tb-widget-config
21 - [typeParameters]="typeParameters"  
22 - [actionSources]="actionSources"  
23 - [isDataEnabled]="isDataEnabled"  
24 - [widgetSettingsSchema]="settingsSchema"  
25 - [dataKeySettingsSchema]="dataKeySettingsSchema"  
26 [aliasController]="aliasController" 21 [aliasController]="aliasController"
27 [functionsOnly]="widgetEditMode" 22 [functionsOnly]="widgetEditMode"
28 [entityAliases]="dashboard.configuration.entityAliases" 23 [entityAliases]="dashboard.configuration.entityAliases"
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; 17 +import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 18 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -29,7 +29,7 @@ import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models @@ -29,7 +29,7 @@ import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models
29 import { WidgetComponentService } from '@home/components/widget/widget-component.service'; 29 import { WidgetComponentService } from '@home/components/widget/widget-component.service';
30 import { WidgetConfigComponentData } from '../../models/widget-component.models'; 30 import { WidgetConfigComponentData } from '../../models/widget-component.models';
31 import { deepClone, isDefined, isString } from '@core/utils'; 31 import { deepClone, isDefined, isString } from '@core/utils';
32 -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 32 +import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
33 import { EntityType } from '@shared/models/entity-type.models'; 33 import { EntityType } from '@shared/models/entity-type.models';
34 import { Observable, of } from 'rxjs'; 34 import { Observable, of } from 'rxjs';
35 import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; 35 import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
@@ -66,21 +66,20 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan @@ -66,21 +66,20 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
66 @Input() 66 @Input()
67 widgetLayout: WidgetLayout; 67 widgetLayout: WidgetLayout;
68 68
69 - @Input() 69 + @ViewChild('widgetForm', {static: true}) widgetForm: NgForm;
  70 +
70 widgetFormGroup: FormGroup; 71 widgetFormGroup: FormGroup;
71 72
72 widgetConfig: WidgetConfigComponentData; 73 widgetConfig: WidgetConfigComponentData;
73 - typeParameters: WidgetTypeParameters;  
74 - actionSources: {[key: string]: WidgetActionSource};  
75 - isDataEnabled: boolean;  
76 - settingsSchema: any;  
77 - dataKeySettingsSchema: any;  
78 - functionsOnly: boolean;  
79 74
80 constructor(protected store: Store<AppState>, 75 constructor(protected store: Store<AppState>,
81 private dialog: MatDialog, 76 private dialog: MatDialog,
  77 + private fb: FormBuilder,
82 private widgetComponentService: WidgetComponentService) { 78 private widgetComponentService: WidgetComponentService) {
83 super(store); 79 super(store);
  80 + this.widgetFormGroup = this.fb.group({
  81 + widgetConfig: [null]
  82 + });
84 } 83 }
85 84
86 ngOnInit(): void { 85 ngOnInit(): void {
@@ -104,27 +103,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan @@ -104,27 +103,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
104 103
105 private loadWidgetConfig() { 104 private loadWidgetConfig() {
106 const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); 105 const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
107 - this.widgetConfig = {  
108 - config: this.widget.config,  
109 - layout: this.widgetLayout,  
110 - widgetType: this.widget.type  
111 - };  
112 - const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;  
113 - const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;  
114 - this.typeParameters = widgetInfo.typeParameters;  
115 - this.actionSources = widgetInfo.actionSources;  
116 - this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;  
117 - if (!settingsSchema || settingsSchema === '') {  
118 - this.settingsSchema = {}; 106 + const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
  107 + const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
  108 + const typeParameters = widgetInfo.typeParameters;
  109 + const actionSources = widgetInfo.actionSources;
  110 + const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
  111 + let settingsSchema;
  112 + if (!rawSettingsSchema || rawSettingsSchema === '') {
  113 + settingsSchema = {};
119 } else { 114 } else {
120 - this.settingsSchema = isString(settingsSchema) ? JSON.parse(settingsSchema) : settingsSchema; 115 + settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema;
121 } 116 }
122 - if (!dataKeySettingsSchema || dataKeySettingsSchema === '') {  
123 - this.dataKeySettingsSchema = {}; 117 + let dataKeySettingsSchema;
  118 + if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') {
  119 + dataKeySettingsSchema = {};
124 } else { 120 } else {
125 - this.dataKeySettingsSchema = isString(dataKeySettingsSchema) ? JSON.parse(dataKeySettingsSchema) : dataKeySettingsSchema; 121 + dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
126 } 122 }
127 - this.functionsOnly = this.dashboard ? false : true; 123 + this.widgetConfig = {
  124 + config: this.widget.config,
  125 + layout: this.widgetLayout,
  126 + widgetType: this.widget.type,
  127 + typeParameters,
  128 + actionSources,
  129 + isDataEnabled,
  130 + settingsSchema,
  131 + dataKeySettingsSchema
  132 + };
128 this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); 133 this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
129 } 134 }
130 135
@@ -13,37 +13,9 @@ @@ -13,37 +13,9 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -@mixin tb-checkered-bg() {  
17 - background-color: #fff;  
18 - background-image:  
19 - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),  
20 - linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);  
21 - background-position: 0 0, 4px 4px;  
22 - background-size: 8px 8px;  
23 -}  
24 16
25 :host { 17 :host {
26 .mat-form-field { 18 .mat-form-field {
27 width: 100%; 19 width: 100%;
28 } 20 }
29 - .tb-color-preview {  
30 - cursor: pointer;  
31 - box-sizing: border-box;  
32 - position: relative;  
33 - width: 24px;  
34 - min-width: 24px;  
35 - height: 24px;  
36 - overflow: hidden;  
37 - content: "";  
38 - border: 2px solid #fff;  
39 - border-radius: 50%;  
40 - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084);  
41 -  
42 - @include tb-checkered-bg();  
43 -  
44 - .tb-color-result {  
45 - width: 100%;  
46 - height: 100%;  
47 - }  
48 - }  
49 } 21 }
@@ -18,8 +18,8 @@ import { @@ -18,8 +18,8 @@ import {
18 Directive, 18 Directive,
19 ElementRef, 19 ElementRef,
20 EventEmitter, 20 EventEmitter,
21 - Input,  
22 - Output, 21 + Input, OnChanges,
  22 + Output, SimpleChanges,
23 ViewContainerRef 23 ViewContainerRef
24 } from '@angular/core'; 24 } from '@angular/core';
25 import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 25 import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; @@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
29 @Directive({ 29 @Directive({
30 selector: '[tb-fullscreen]' 30 selector: '[tb-fullscreen]'
31 }) 31 })
32 -export class FullscreenDirective { 32 +export class FullscreenDirective implements OnChanges {
33 33
34 fullscreenValue = false; 34 fullscreenValue = false;
35 35
@@ -37,16 +37,10 @@ export class FullscreenDirective { @@ -37,16 +37,10 @@ export class FullscreenDirective {
37 private parentElement: HTMLElement; 37 private parentElement: HTMLElement;
38 38
39 @Input() 39 @Input()
40 - set fullscreen(fullscreen: boolean) {  
41 - if (this.fullscreenValue !== fullscreen) {  
42 - this.fullscreenValue = fullscreen;  
43 - if (this.fullscreenValue) {  
44 - this.enterFullscreen();  
45 - } else {  
46 - this.exitFullscreen();  
47 - }  
48 - }  
49 - } 40 + fullscreen: boolean;
  41 +
  42 + @Input()
  43 + fullscreenElement: HTMLElement;
50 44
51 @Output() 45 @Output()
52 fullscreenChanged = new EventEmitter<boolean>(); 46 fullscreenChanged = new EventEmitter<boolean>();
@@ -56,11 +50,30 @@ export class FullscreenDirective { @@ -56,11 +50,30 @@ export class FullscreenDirective {
56 private overlay: Overlay) { 50 private overlay: Overlay) {
57 } 51 }
58 52
  53 + ngOnChanges(changes: SimpleChanges): void {
  54 + let updateFullscreen = false;
  55 + for (const propName of Object.keys(changes)) {
  56 + const change = changes[propName];
  57 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  58 + if (propName === 'fullscreen') {
  59 + updateFullscreen = true;
  60 + }
  61 + }
  62 + }
  63 + if (updateFullscreen) {
  64 + if (this.fullscreen) {
  65 + this.enterFullscreen();
  66 + } else {
  67 + this.exitFullscreen();
  68 + }
  69 + }
  70 + }
  71 +
59 enterFullscreen() { 72 enterFullscreen() {
60 - const targetElement = this.elementRef;  
61 - this.parentElement = targetElement.nativeElement.parentElement;  
62 - this.parentElement.removeChild(targetElement.nativeElement);  
63 - targetElement.nativeElement.classList.add('tb-fullscreen'); 73 + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
  74 + this.parentElement = targetElement.parentElement;
  75 + this.parentElement.removeChild(targetElement);
  76 + targetElement.classList.add('tb-fullscreen');
64 const position = this.overlay.position(); 77 const position = this.overlay.position();
65 const config = new OverlayConfig({ 78 const config = new OverlayConfig({
66 hasBackdrop: false, 79 hasBackdrop: false,
@@ -73,19 +86,19 @@ export class FullscreenDirective { @@ -73,19 +86,19 @@ export class FullscreenDirective {
73 86
74 this.overlayRef = this.overlay.create(config); 87 this.overlayRef = this.overlay.create(config);
75 this.overlayRef.attach(new EmptyPortal()); 88 this.overlayRef.attach(new EmptyPortal());
76 - this.overlayRef.overlayElement.append( targetElement.nativeElement ); 89 + this.overlayRef.overlayElement.append( targetElement );
77 this.fullscreenChanged.emit(true); 90 this.fullscreenChanged.emit(true);
78 } 91 }
79 92
80 exitFullscreen() { 93 exitFullscreen() {
81 - const targetElement = this.elementRef; 94 + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
82 if (this.parentElement) { 95 if (this.parentElement) {
83 - this.overlayRef.overlayElement.removeChild( targetElement.nativeElement );  
84 - this.parentElement.append(targetElement.nativeElement); 96 + this.overlayRef.overlayElement.removeChild( targetElement );
  97 + this.parentElement.append(targetElement);
85 this.parentElement = null; 98 this.parentElement = null;
86 } 99 }
87 - targetElement.nativeElement.classList.remove('tb-fullscreen');  
88 - if (this.elementRef !== targetElement) { 100 + targetElement.classList.remove('tb-fullscreen');
  101 + if (this.elementRef) {
89 this.elementRef.nativeElement.classList.remove('tb-fullscreen'); 102 this.elementRef.nativeElement.classList.remove('tb-fullscreen');
90 } 103 }
91 this.overlayRef.dispose(); 104 this.overlayRef.dispose();
  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 +
  18 +export interface JsonFormComponentData {
  19 + model?: any;
  20 + schema?: any;
  21 + form?: any;
  22 + groupInfoes?: any[];
  23 +}
@@ -15,8 +15,8 @@ @@ -15,8 +15,8 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreen]="isFullscreen"  
19 - (fullscreenChanged)="onFullscreenChanged()"> 18 +<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreenElement]="targetFullscreenElement"
  19 + [fullscreen]="isFullscreen"
  20 + (fullscreenChanged)="onFullscreenChanged($event)">
20 <div #reactRoot></div> 21 <div #reactRoot></div>
21 - <div>{{ model | json }}</div>  
22 </div> 22 </div>
@@ -40,6 +40,7 @@ import * as React from 'react'; @@ -40,6 +40,7 @@ import * as React from 'react';
40 import * as ReactDOM from 'react-dom'; 40 import * as ReactDOM from 'react-dom';
41 import ReactSchemaForm from './react/json-form-react'; 41 import ReactSchemaForm from './react/json-form-react';
42 import JsonFormUtils from './react/json-form-utils'; 42 import JsonFormUtils from './react/json-form-utils';
  43 +import { JsonFormComponentData } from './json-form-component.models';
43 44
44 @Component({ 45 @Component({
45 selector: 'tb-json-form', 46 selector: 'tb-json-form',
@@ -64,12 +65,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -64,12 +65,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
64 @ViewChild('reactRoot', {static: true}) 65 @ViewChild('reactRoot', {static: true})
65 reactRootElmRef: ElementRef<HTMLElement>; 66 reactRootElmRef: ElementRef<HTMLElement>;
66 67
67 - @Input() schema: any;  
68 -  
69 - @Input() form: any;  
70 -  
71 - @Input() groupInfoes: any[];  
72 -  
73 private readonlyValue: boolean; 68 private readonlyValue: boolean;
74 get readonly(): boolean { 69 get readonly(): boolean {
75 return this.readonlyValue; 70 return this.readonlyValue;
@@ -91,11 +86,18 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -91,11 +86,18 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
91 onToggleFullscreen: this.onToggleFullscreen.bind(this) 86 onToggleFullscreen: this.onToggleFullscreen.bind(this)
92 }; 87 };
93 88
  89 + data: JsonFormComponentData;
  90 +
94 model: any; 91 model: any;
  92 + schema: any;
  93 + form: any;
  94 + groupInfoes: any[];
95 95
96 isModelValid = true; 96 isModelValid = true;
97 97
98 isFullscreen = false; 98 isFullscreen = false;
  99 + targetFullscreenElement: HTMLElement;
  100 + fullscreenFinishFn: () => void;
99 101
100 private propagateChange = null; 102 private propagateChange = null;
101 103
@@ -128,77 +130,91 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato @@ -128,77 +130,91 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
128 }; 130 };
129 } 131 }
130 132
131 - writeValue(value: any): void {  
132 - this.model = value || {}; 133 + writeValue(data: JsonFormComponentData): void {
  134 + this.data = data;
  135 + this.schema = this.data && this.data.schema ? deepClone(this.data.schema) : {
  136 + type: 'object'
  137 + };
  138 + this.schema.strict = true;
  139 + this.form = this.data && this.data.form ? deepClone(this.data.form) : [ '*' ];
  140 + this.groupInfoes = this.data && this.data.groupInfoes ? deepClone(this.data.groupInfoes) : [];
  141 + this.model = this.data && this.data.model || {};
133 this.model = inspector.sanitize(this.schema, this.model).data; 142 this.model = inspector.sanitize(this.schema, this.model).data;
134 this.updateAndRender(); 143 this.updateAndRender();
135 this.isModelValid = this.validateModel(); 144 this.isModelValid = this.validateModel();
136 if (!this.isModelValid) { 145 if (!this.isModelValid) {
137 this.updateView(); 146 this.updateView();
138 } 147 }
139 - } 148 +}
140 149
141 updateView() { 150 updateView() {
142 - this.propagateChange(this.model); 151 + if (this.data) {
  152 + this.data.model = this.model;
  153 + this.propagateChange(this.data);
  154 + }
143 } 155 }
144 156
145 ngOnChanges(changes: SimpleChanges): void { 157 ngOnChanges(changes: SimpleChanges): void {
146 for (const propName of Object.keys(changes)) { 158 for (const propName of Object.keys(changes)) {
147 const change = changes[propName]; 159 const change = changes[propName];
148 if (!change.firstChange && change.currentValue !== change.previousValue) { 160 if (!change.firstChange && change.currentValue !== change.previousValue) {
149 - if (propName === 'schema') {  
150 - this.model = inspector.sanitize(this.schema, this.model).data;  
151 - this.isModelValid = this.validateModel();  
152 - }  
153 - if (['readonly', 'schema', 'form', 'groupInfoes'].includes(propName)) { 161 + if (propName === 'readonly') {
154 this.updateAndRender(); 162 this.updateAndRender();
155 } 163 }
156 } 164 }
157 } 165 }
158 } 166 }
159 167
160 - onFullscreenChanged() {}  
161 -  
162 private onModelChange(key: (string | number)[], val: any) { 168 private onModelChange(key: (string | number)[], val: any) {
163 if (isString(val) && val === '') { 169 if (isString(val) && val === '') {
164 val = undefined; 170 val = undefined;
165 } 171 }
166 if (JsonFormUtils.updateValue(key, this.model, val)) { 172 if (JsonFormUtils.updateValue(key, this.model, val)) {
167 this.formProps.model = this.model; 173 this.formProps.model = this.model;
  174 + this.isModelValid = this.validateModel();
168 this.updateView(); 175 this.updateView();
169 } 176 }
170 } 177 }
171 178
172 - private onColorClick(event: MouseEvent, key: string, val: string) { 179 + private onColorClick(key: (string | number)[],
  180 + val: tinycolor.ColorFormats.RGBA,
  181 + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
173 this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { 182 this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => {
174 - const e = event as any;  
175 - if (e.data && e.data.onValueChanged) {  
176 - e.data.onValueChanged(tinycolor(color).toRgb()); 183 + if (colorSelectedFn) {
  184 + colorSelectedFn(tinycolor(color).toRgb());
177 } 185 }
178 }); 186 });
179 } 187 }
180 188
181 - private onToggleFullscreen() { 189 + private onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) {
  190 + this.targetFullscreenElement = element;
182 this.isFullscreen = !this.isFullscreen; 191 this.isFullscreen = !this.isFullscreen;
183 - this.formProps.isFullscreen = this.isFullscreen; 192 + this.fullscreenFinishFn = fullscreenFinishFn;
  193 + }
  194 +
  195 + onFullscreenChanged(fullscreen: boolean) {
  196 + this.formProps.isFullscreen = fullscreen;
  197 + this.renderReactSchemaForm(false);
  198 + if (this.fullscreenFinishFn) {
  199 + this.fullscreenFinishFn();
  200 + this.fullscreenFinishFn = null;
  201 + }
184 } 202 }
185 203
186 private updateAndRender() { 204 private updateAndRender() {
187 - const schema = this.schema ? deepClone(this.schema) : {  
188 - type: 'object'  
189 - };  
190 - schema.strict = true;  
191 - const form = this.form ? deepClone(this.form) : [ '*' ];  
192 - const groupInfoes = this.groupInfoes ? deepClone(this.groupInfoes) : []; 205 +
193 this.formProps.option.formDefaults.readonly = this.readonly; 206 this.formProps.option.formDefaults.readonly = this.readonly;
194 - this.formProps.schema = schema;  
195 - this.formProps.form = form;  
196 - this.formProps.groupInfoes = groupInfoes; 207 + this.formProps.schema = this.schema;
  208 + this.formProps.form = this.form;
  209 + this.formProps.groupInfoes = this.groupInfoes;
197 this.formProps.model = deepClone(this.model); 210 this.formProps.model = deepClone(this.model);
198 this.renderReactSchemaForm(); 211 this.renderReactSchemaForm();
199 } 212 }
200 213
201 - private renderReactSchemaForm() { 214 + private renderReactSchemaForm(destroy: boolean = true) {
  215 + if (destroy) {
  216 + this.destroyReactSchemaForm();
  217 + }
202 ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement); 218 ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement);
203 } 219 }
204 220
  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 * as React from 'react';
  18 +import ThingsboardBaseComponent from './json-form-base-component';
  19 +import reactCSS from 'reactcss';
  20 +import ReactAce from 'react-ace';
  21 +import Button from '@material-ui/core/Button';
  22 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  23 +import * as tinycolor from 'tinycolor2';
  24 +import { IEditorProps } from 'react-ace/src/types';
  25 +
  26 +interface ThingsboardAceEditorProps extends JsonFormFieldProps {
  27 + mode: string;
  28 + onTidy: (value: string) => string;
  29 +}
  30 +
  31 +interface ThingsboardAceEditorState extends JsonFormFieldState {
  32 + isFull: boolean;
  33 + focused: boolean;
  34 +}
  35 +
  36 +class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, ThingsboardAceEditorState> {
  37 +
  38 + hostElement: HTMLElement;
  39 + private aceEditor: IEditorProps;
  40 +
  41 + constructor(props) {
  42 + super(props);
  43 + this.onValueChanged = this.onValueChanged.bind(this);
  44 + this.onBlur = this.onBlur.bind(this);
  45 + this.onFocus = this.onFocus.bind(this);
  46 + this.onTidy = this.onTidy.bind(this);
  47 + this.onLoad = this.onLoad.bind(this);
  48 + this.onToggleFull = this.onToggleFull.bind(this);
  49 + const value = props.value ? props.value + '' : '';
  50 + this.state = {
  51 + isFull: false,
  52 + value,
  53 + focused: false
  54 + };
  55 + }
  56 +
  57 + onValueChanged(value) {
  58 + this.setState({
  59 + value
  60 + });
  61 + this.props.onChangeValidate({
  62 + target: {
  63 + value
  64 + }
  65 + });
  66 + }
  67 +
  68 + onBlur() {
  69 + this.setState({ focused: false });
  70 + }
  71 +
  72 + onFocus() {
  73 + this.setState({ focused: true });
  74 + }
  75 +
  76 + onTidy() {
  77 + if (!this.props.form.readonly) {
  78 + let value = this.state.value;
  79 + value = this.props.onTidy(value);
  80 + this.setState({
  81 + value
  82 + });
  83 + this.props.onChangeValidate({
  84 + target: {
  85 + value
  86 + }
  87 + });
  88 + }
  89 + }
  90 +
  91 + onLoad(editor: IEditorProps) {
  92 + this.aceEditor = editor;
  93 + }
  94 +
  95 + onToggleFull() {
  96 + this.setState({ isFull: !this.state.isFull });
  97 + this.props.onToggleFullscreen(this.hostElement, () => {
  98 + if (this.aceEditor) {
  99 + this.aceEditor.resize();
  100 + this.aceEditor.renderer.updateFull();
  101 + }
  102 + });
  103 + }
  104 +
  105 + componentDidUpdate() {
  106 + }
  107 +
  108 + render() {
  109 +
  110 + const styles = reactCSS({
  111 + default: {
  112 + tidyButtonStyle: {
  113 + color: '#7B7B7B',
  114 + minWidth: '32px',
  115 + minHeight: '15px',
  116 + lineHeight: '15px',
  117 + fontSize: '0.800rem',
  118 + margin: '0',
  119 + padding: '4px',
  120 + height: '23px',
  121 + borderRadius: '5px',
  122 + marginLeft: '5px'
  123 + }
  124 + }
  125 + });
  126 +
  127 + let labelClass = 'tb-label';
  128 + if (this.props.form.required) {
  129 + labelClass += ' tb-required';
  130 + }
  131 + if (this.props.form.readonly) {
  132 + labelClass += ' tb-readonly';
  133 + }
  134 + if (this.state.focused) {
  135 + labelClass += ' tb-focused';
  136 + }
  137 + let containerClass = 'tb-container';
  138 + const style = this.props.form.style || {width: '100%'};
  139 + if (this.state.isFull) {
  140 + containerClass += ' fullscreen-form-field';
  141 + }
  142 + return (
  143 + <div>
  144 + <div className='tb-json-form' ref={c => (this.hostElement = c)}>
  145 + <div className={containerClass}>
  146 + <label className={labelClass}>{this.props.form.title}</label>
  147 + <div className='json-form-ace-editor'>
  148 + <div className='title-panel'>
  149 + <label>{this.props.mode}</label>
  150 + <Button style={ styles.tidyButtonStyle }
  151 + className='tidy-button' onClick={this.onTidy}>Tidy</Button>
  152 + <Button style={ styles.tidyButtonStyle }
  153 + className='tidy-button' onClick={this.onToggleFull}>
  154 + {this.state.isFull ?
  155 + 'Exit fullscreen' : 'Fullscreen'}
  156 + </Button>
  157 + </div>
  158 + <ReactAce mode={this.props.mode}
  159 + height={this.state.isFull ? '100%' : '150px'}
  160 + width={this.state.isFull ? '100%' : '300px'}
  161 + theme='github'
  162 + onChange={this.onValueChanged}
  163 + onFocus={this.onFocus}
  164 + onBlur={this.onBlur}
  165 + onLoad={this.onLoad}
  166 + name={this.props.form.title}
  167 + value={this.state.value}
  168 + readOnly={this.props.form.readonly}
  169 + editorProps={{$blockScrolling: Infinity}}
  170 + enableBasicAutocompletion={true}
  171 + enableSnippets={true}
  172 + enableLiveAutocompletion={true}
  173 + style={style}/>
  174 + </div>
  175 + <div className='json-form-error'
  176 + style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div>
  177 + </div>
  178 + </div>
  179 + </div>
  180 + );
  181 + }
  182 +}
  183 +
  184 +export default ThingsboardBaseComponent(ThingsboardAceEditor);
@@ -17,7 +17,8 @@ import * as React from 'react'; @@ -17,7 +17,8 @@ import * as React from 'react';
17 import JsonFormUtils from './json-form-utils'; 17 import JsonFormUtils from './json-form-utils';
18 import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; 18 import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
19 19
20 -export default ThingsboardBaseComponent => class extends React.Component<JsonFormFieldProps, JsonFormFieldState> { 20 +export default ThingsboardBaseComponent => class<P extends JsonFormFieldProps>
  21 + extends React.Component<P, JsonFormFieldState> {
21 22
22 constructor(props) { 23 constructor(props) {
23 super(props); 24 super(props);
  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 +import * as React from 'react';
  17 +import * as ReactDOM from 'react-dom';
  18 +import ThingsboardBaseComponent from './json-form-base-component';
  19 +import reactCSS from 'reactcss';
  20 +import * as tinycolor from 'tinycolor2';
  21 +import TextField from '@material-ui/core/TextField';
  22 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  23 +import IconButton from '@material-ui/core/IconButton';
  24 +import ClearIcon from '@material-ui/icons/Clear';
  25 +import Tooltip from '@material-ui/core/Tooltip';
  26 +
  27 +interface ThingsboardColorState extends JsonFormFieldState {
  28 + color: tinycolor.ColorFormats.RGBA | null;
  29 + focused: boolean;
  30 +}
  31 +
  32 +class ThingsboardColor extends React.Component<JsonFormFieldProps, ThingsboardColorState> {
  33 +
  34 + constructor(props) {
  35 + super(props);
  36 + this.onBlur = this.onBlur.bind(this);
  37 + this.onFocus = this.onFocus.bind(this);
  38 + this.onValueChanged = this.onValueChanged.bind(this);
  39 + this.onSwatchClick = this.onSwatchClick.bind(this);
  40 + this.onClear = this.onClear.bind(this);
  41 + const value = props.value ? props.value + '' : null;
  42 + const color = value != null ? tinycolor(value).toRgb() : null;
  43 + this.state = {
  44 + color,
  45 + focused: false
  46 + };
  47 + }
  48 +
  49 + onBlur() {
  50 + this.setState({focused: false});
  51 + }
  52 +
  53 + onFocus() {
  54 + this.setState({focused: true});
  55 + }
  56 +
  57 + componentDidMount() {
  58 + const node = ReactDOM.findDOMNode(this);
  59 + const colContainer = $(node).children('#color-container');
  60 + colContainer.click((event) => {
  61 + if (!this.props.form.readonly) {
  62 + this.onSwatchClick(event);
  63 + }
  64 + });
  65 + }
  66 +
  67 + componentWillUnmount() {
  68 + const node = ReactDOM.findDOMNode(this);
  69 + const colContainer = $(node).children('#color-container');
  70 + colContainer.off( 'click' );
  71 + }
  72 +
  73 + onValueChanged(value: tinycolor.ColorFormats.RGBA | null) {
  74 + let color: tinycolor.Instance = null;
  75 + if (value != null) {
  76 + color = tinycolor(value);
  77 + }
  78 + this.setState({
  79 + color: value
  80 + });
  81 + let colorValue = '';
  82 + if (color != null && color.getAlpha() !== 1) {
  83 + colorValue = color.toRgbString();
  84 + } else if (color != null) {
  85 + colorValue = color.toHexString();
  86 + }
  87 + this.props.onChangeValidate({
  88 + target: {
  89 + value: colorValue
  90 + }
  91 + });
  92 + }
  93 +
  94 + onSwatchClick(event) {
  95 + this.props.onColorClick(this.props.form.key, this.state.color,
  96 + (color) => {
  97 + this.onValueChanged(color);
  98 + }
  99 + );
  100 + }
  101 +
  102 + onClear(event) {
  103 + if (event) {
  104 + event.stopPropagation();
  105 + }
  106 + this.onValueChanged(null);
  107 + }
  108 +
  109 + render() {
  110 +
  111 + let background = 'rgba(0,0,0,0)';
  112 + if (this.state.color != null) {
  113 + background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`;
  114 + }
  115 +
  116 + const styles = reactCSS({
  117 + default: {
  118 + color: {
  119 + background: `${ background }`
  120 + },
  121 + swatch: {
  122 + display: 'inline-block',
  123 + marginRight: '10px',
  124 + marginTop: 'auto',
  125 + marginBottom: 'auto',
  126 + cursor: 'pointer',
  127 + opacity: `${ this.props.form.readonly ? '0.6' : '1' }`
  128 + },
  129 + swatchText: {
  130 + width: '100%'
  131 + },
  132 + container: {
  133 + display: 'flex',
  134 + flexDirection: 'row',
  135 + alignItems: 'center'
  136 + },
  137 + colorContainer: {
  138 + display: 'flex',
  139 + width: '100%'
  140 + }
  141 + },
  142 + });
  143 +
  144 + let fieldClass = 'tb-field';
  145 + if (this.props.form.required) {
  146 + fieldClass += ' tb-required';
  147 + }
  148 + if (this.props.form.readonly) {
  149 + fieldClass += ' tb-readonly';
  150 + }
  151 + if (this.state.focused) {
  152 + fieldClass += ' tb-focused';
  153 + }
  154 +
  155 + let stringColor = '';
  156 + if (this.state.color != null) {
  157 + const color = tinycolor(this.state.color);
  158 + stringColor = color.toRgbString();
  159 + }
  160 +
  161 + return (
  162 + <div style={ styles.container }>
  163 + <div id='color-container' style={ styles.colorContainer }>
  164 + <div className='tb-color-preview' style={ styles.swatch }>
  165 + <div className='tb-color-result' style={ styles.color }/>
  166 + </div>
  167 + <TextField
  168 + className={fieldClass}
  169 + label={this.props.form.title}
  170 + error={!this.props.valid}
  171 + helperText={this.props.valid ? this.props.form.placeholder : this.props.error}
  172 + value={stringColor}
  173 + disabled={this.props.form.readonly}
  174 + onFocus={this.onFocus}
  175 + onBlur={this.onBlur}
  176 + style={ styles.swatchText }/>
  177 + </div>
  178 + <Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><ClearIcon/></IconButton></Tooltip>
  179 + </div>
  180 + );
  181 + }
  182 +}
  183 +
  184 +export default ThingsboardBaseComponent(ThingsboardColor);
  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 +import * as React from 'react';
  17 +import ThingsboardAceEditor from './json-form-ace-editor';
  18 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  19 +import { css_beautify } from 'js-beautify';
  20 +
  21 +class ThingsboardCss extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
  22 +
  23 + constructor(props) {
  24 + super(props);
  25 + this.onTidyCss = this.onTidyCss.bind(this);
  26 + }
  27 +
  28 + onTidyCss(css: string): string {
  29 + return css_beautify(css, {indent_size: 4});
  30 + }
  31 +
  32 + render() {
  33 + return (
  34 + <ThingsboardAceEditor {...this.props} mode='css' onTidy={this.onTidyCss} {...this.state}></ThingsboardAceEditor>
  35 + );
  36 + }
  37 +}
  38 +
  39 +export default ThingsboardCss;
  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 +import * as React from 'react';
  17 +import ThingsboardAceEditor from './json-form-ace-editor';
  18 +import { html_beautify } from 'js-beautify';
  19 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  20 +
  21 +class ThingsboardHtml extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
  22 +
  23 + constructor(props) {
  24 + super(props);
  25 + this.onTidyHtml = this.onTidyHtml.bind(this);
  26 + }
  27 +
  28 + onTidyHtml(html: string): string {
  29 + return html_beautify(html, {indent_size: 4});
  30 + }
  31 +
  32 + render() {
  33 + return (
  34 + <ThingsboardAceEditor {...this.props} mode='html' onTidy={this.onTidyHtml} {...this.state}></ThingsboardAceEditor>
  35 + );
  36 + }
  37 +}
  38 +
  39 +export default ThingsboardHtml;
  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 +import React, {useCallback} from 'react';
  17 +import { DropzoneState, useDropzone } from 'react-dropzone';
  18 +import ThingsboardBaseComponent from './json-form-base-component';
  19 +import Dropzone from 'react-dropzone';
  20 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  21 +import IconButton from '@material-ui/core/IconButton';
  22 +import ClearIcon from '@material-ui/icons/Clear';
  23 +import Tooltip from '@material-ui/core/Tooltip';
  24 +
  25 +interface ThingsboardImageState extends JsonFormFieldState {
  26 + imageUrl: string;
  27 +}
  28 +
  29 +class ThingsboardImage extends React.Component<JsonFormFieldProps, ThingsboardImageState> {
  30 +
  31 + constructor(props) {
  32 + super(props);
  33 + this.onDrop = this.onDrop.bind(this);
  34 + this.onClear = this.onClear.bind(this);
  35 + const value = props.value ? props.value + '' : null;
  36 + this.state = {
  37 + imageUrl: value
  38 + };
  39 + }
  40 +
  41 + onDrop(acceptedFiles: File[]) {
  42 + const reader = new FileReader();
  43 + reader.onload = () => {
  44 + this.onValueChanged(reader.result);
  45 + };
  46 + reader.readAsDataURL(acceptedFiles[0]);
  47 + }
  48 +
  49 + onValueChanged(value) {
  50 + this.setState({
  51 + imageUrl: value
  52 + });
  53 + this.props.onChangeValidate({
  54 + target: {
  55 + value
  56 + }
  57 + });
  58 + }
  59 +
  60 + onClear(event) {
  61 + if (event) {
  62 + event.stopPropagation();
  63 + }
  64 + this.onValueChanged('');
  65 + }
  66 +
  67 + render() {
  68 +
  69 + let labelClass = 'tb-label';
  70 + if (this.props.form.required) {
  71 + labelClass += ' tb-required';
  72 + }
  73 + if (this.props.form.readonly) {
  74 + labelClass += ' tb-readonly';
  75 + }
  76 +
  77 + let previewComponent;
  78 + if (this.state.imageUrl) {
  79 + previewComponent = <img className='tb-image-preview' src={this.state.imageUrl} />;
  80 + } else {
  81 + previewComponent = <div>No image selected</div>;
  82 + }
  83 +
  84 + return (
  85 + <div className='tb-container'>
  86 + <label className={labelClass}>{this.props.form.title}</label>
  87 + <div className='tb-image-select-container'>
  88 + <div className='tb-image-preview-container'>{previewComponent}</div>
  89 + <div className='tb-image-clear-container'>
  90 + <Tooltip title='Clear' placement='top'>
  91 + <IconButton className='tb-image-clear-btn' onClick={this.onClear}><ClearIcon/></IconButton>
  92 + </Tooltip>
  93 + </div>
  94 + <Dropzone onDrop={this.onDrop}
  95 + accept='image/*' multiple={false}>
  96 + {({getRootProps, getInputProps}) => (
  97 + <div className='tb-dropzone' {...getRootProps()}>
  98 + <div>Drop an image or click to select a file to upload.</div>
  99 + <input {...getInputProps()} />
  100 + </div>
  101 + )}
  102 + </Dropzone>
  103 + </div>
  104 + </div>
  105 + );
  106 + }
  107 +}
  108 +
  109 +export default ThingsboardBaseComponent(ThingsboardImage);
  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 +import * as React from 'react';
  17 +import ThingsboardAceEditor from './json-form-ace-editor';
  18 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  19 +
  20 +class ThingsboardJavaScript extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
  21 +
  22 + constructor(props) {
  23 + super(props);
  24 + this.onTidyJavascript = this.onTidyJavascript.bind(this);
  25 + }
  26 +
  27 + onTidyJavascript(javascript: string): string {
  28 + return js_beautify(javascript, {indent_size: 4, wrap_line_length: 60});
  29 + }
  30 +
  31 + render() {
  32 + return (
  33 + <ThingsboardAceEditor {...this.props} mode='javascript' onTidy={this.onTidyJavascript} {...this.state}></ThingsboardAceEditor>
  34 + );
  35 + }
  36 +}
  37 +
  38 +export default ThingsboardJavaScript;
  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 +import * as React from 'react';
  17 +import ThingsboardAceEditor from './json-form-ace-editor';
  18 +import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
  19 +
  20 +class ThingsboardJson extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
  21 +
  22 + constructor(props) {
  23 + super(props);
  24 + this.onTidyJson = this.onTidyJson.bind(this);
  25 + }
  26 +
  27 + onTidyJson(json: string): string {
  28 + return js_beautify(json, {indent_size: 4});
  29 + }
  30 +
  31 + render() {
  32 + return (
  33 + <ThingsboardAceEditor {...this.props} mode='json' onTidy={this.onTidyJson} {...this.state}></ThingsboardAceEditor>
  34 + );
  35 + }
  36 +}
  37 +
  38 +export default ThingsboardJson;
@@ -17,24 +17,25 @@ import * as React from 'react'; @@ -17,24 +17,25 @@ import * as React from 'react';
17 import JsonFormUtils from './json-form-utils'; 17 import JsonFormUtils from './json-form-utils';
18 18
19 import ThingsboardArray from './json-form-array'; 19 import ThingsboardArray from './json-form-array';
20 -/*import ThingsboardJavaScript from './json-form-javascript.jsx';  
21 -import ThingsboardJson from './json-form-json.jsx';  
22 -import ThingsboardHtml from './json-form-html.jsx';  
23 -import ThingsboardCss from './json-form-css.jsx';  
24 -import ThingsboardColor from './json-form-color.jsx'*/ 20 +import ThingsboardJavaScript from './json-form-javascript';
  21 +import ThingsboardJson from './json-form-json';
  22 +import ThingsboardHtml from './json-form-html';
  23 +import ThingsboardCss from './json-form-css';
  24 +import ThingsboardColor from './json-form-color';
25 import ThingsboardRcSelect from './json-form-rc-select'; 25 import ThingsboardRcSelect from './json-form-rc-select';
26 import ThingsboardNumber from './json-form-number'; 26 import ThingsboardNumber from './json-form-number';
27 import ThingsboardText from './json-form-text'; 27 import ThingsboardText from './json-form-text';
28 import ThingsboardSelect from './json-form-select'; 28 import ThingsboardSelect from './json-form-select';
29 import ThingsboardRadios from './json-form-radios'; 29 import ThingsboardRadios from './json-form-radios';
30 import ThingsboardDate from './json-form-date'; 30 import ThingsboardDate from './json-form-date';
31 -/*import ThingsboardImage from './json-form-image.jsx';*/ 31 +import ThingsboardImage from './json-form-image';
32 import ThingsboardCheckbox from './json-form-checkbox'; 32 import ThingsboardCheckbox from './json-form-checkbox';
33 import ThingsboardHelp from './json-form-help'; 33 import ThingsboardHelp from './json-form-help';
34 import ThingsboardFieldSet from './json-form-fieldset'; 34 import ThingsboardFieldSet from './json-form-fieldset';
35 -import { JsonFormProps, GroupInfo, JsonFormData } from './json-form.models'; 35 +import { JsonFormProps, GroupInfo, JsonFormData, onChangeFn, OnColorClickFn } from './json-form.models';
36 36
37 import _ from 'lodash'; 37 import _ from 'lodash';
  38 +import * as tinycolor from 'tinycolor2';
38 39
39 class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { 40 class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
40 41
@@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { @@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
52 select: ThingsboardSelect, 53 select: ThingsboardSelect,
53 radios: ThingsboardRadios, 54 radios: ThingsboardRadios,
54 date: ThingsboardDate, 55 date: ThingsboardDate,
55 - // image: ThingsboardImage, 56 + image: ThingsboardImage,
56 checkbox: ThingsboardCheckbox, 57 checkbox: ThingsboardCheckbox,
57 help: ThingsboardHelp, 58 help: ThingsboardHelp,
58 array: ThingsboardArray, 59 array: ThingsboardArray,
59 - // javascript: ThingsboardJavaScript,  
60 - // json: ThingsboardJson,  
61 - // html: ThingsboardHtml,  
62 - // css: ThingsboardCss,  
63 - // color: ThingsboardColor, 60 + javascript: ThingsboardJavaScript,
  61 + json: ThingsboardJson,
  62 + html: ThingsboardHtml,
  63 + css: ThingsboardCss,
  64 + color: ThingsboardColor,
64 'rc-select': ThingsboardRcSelect, 65 'rc-select': ThingsboardRcSelect,
65 fieldset: ThingsboardFieldSet 66 fieldset: ThingsboardFieldSet
66 }; 67 };
@@ -78,20 +79,21 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { @@ -78,20 +79,21 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
78 } 79 }
79 } 80 }
80 81
81 - onColorClick(event, key, val) {  
82 - this.props.onColorClick(event, key, val); 82 + onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
  83 + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
  84 + this.props.onColorClick(key, val, colorSelectedFn);
83 } 85 }
84 86
85 - onToggleFullscreen() {  
86 - this.props.onToggleFullscreen(); 87 + onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) {
  88 + this.props.onToggleFullscreen(element, fullscreenFinishFn);
87 } 89 }
88 90
89 91
90 builder(form: JsonFormData, 92 builder(form: JsonFormData,
91 model: any, 93 model: any,
92 index: number, 94 index: number,
93 - onChange: (key: (string | number)[], val: any) => void,  
94 - onColorClick: (event: MouseEvent, key: string, val: string) => void, 95 + onChange: onChangeFn,
  96 + onColorClick: OnColorClickFn,
95 onToggleFullscreen: () => void, 97 onToggleFullscreen: () => void,
96 mapper: {[type: string]: any}): JSX.Element { 98 mapper: {[type: string]: any}): JSX.Element {
97 const type = form.type; 99 const type = form.type;
@@ -173,9 +175,9 @@ class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps @@ -173,9 +175,9 @@ class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps
173 } 175 }
174 176
175 render() { 177 render() {
176 - const theCla = 'pull-right fa fa-chevron-down md-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); 178 + const theCla = 'pull-right fa fa-chevron-down tb-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled');
177 return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}> 179 return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}>
178 - <div className='SchemaGroupname md-button-toggle' 180 + <div className='SchemaGroupname tb-button-toggle'
179 onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div> 181 onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
180 <div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div> 182 <div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div>
181 </section>); 183 </section>);
@@ -544,11 +544,13 @@ function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[]) @@ -544,11 +544,13 @@ function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[])
544 544
545 const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => { 545 const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => {
546 $fn($schema, $path); 546 $fn($schema, $path);
547 - for (const k of Object.keys($schema.properties)) {  
548 - if ($schema.properties.hasOwnProperty(k)) {  
549 - const currentPath = $path.slice();  
550 - currentPath.push(k);  
551 - traverse($schema.properties[k], $fn, currentPath); 547 + if ($schema.properties) {
  548 + for (const k of Object.keys($schema.properties)) {
  549 + if ($schema.properties.hasOwnProperty(k)) {
  550 + const currentPath = $path.slice();
  551 + currentPath.push(k);
  552 + traverse($schema.properties[k], $fn, currentPath);
  553 + }
552 } 554 }
553 } 555 }
554 if (!ignoreArrays && $schema.items) { 556 if (!ignoreArrays && $schema.items) {
@@ -18,12 +18,13 @@ import { isUndefined, isDefined, isString } from '@app/core/utils'; @@ -18,12 +18,13 @@ import { isUndefined, isDefined, isString } from '@app/core/utils';
18 import * as equal from 'deep-equal'; 18 import * as equal from 'deep-equal';
19 import ObjectPath from 'objectpath'; 19 import ObjectPath from 'objectpath';
20 import * as React from 'react'; 20 import * as React from 'react';
  21 +import * as tinycolor from 'tinycolor2';
21 22
22 export interface SchemaValidationResult { 23 export interface SchemaValidationResult {
23 valid: boolean; 24 valid: boolean;
24 error?: { 25 error?: {
25 message?: string; 26 message?: string;
26 - } 27 + };
27 } 28 }
28 29
29 export interface FormOption { 30 export interface FormOption {
@@ -47,6 +48,11 @@ export interface GroupInfo { @@ -47,6 +48,11 @@ export interface GroupInfo {
47 GroupTitle: string; 48 GroupTitle: string;
48 } 49 }
49 50
  51 +export type onChangeFn = (key: (string | number)[], val: any) => void;
  52 +export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
  53 + colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void;
  54 +export type onToggleFullscreenFn = (element: HTMLElement, fullscreenFinishFn?: () => void) => void;
  55 +
50 export interface JsonFormProps { 56 export interface JsonFormProps {
51 model?: any; 57 model?: any;
52 schema?: any; 58 schema?: any;
@@ -55,9 +61,9 @@ export interface JsonFormProps { @@ -55,9 +61,9 @@ export interface JsonFormProps {
55 isFullscreen: boolean; 61 isFullscreen: boolean;
56 ignore?: {[key: string]: boolean}; 62 ignore?: {[key: string]: boolean};
57 option: FormOption; 63 option: FormOption;
58 - onModelChange?: (key: (string | number)[], val: any) => void;  
59 - onColorClick?: (event: MouseEvent, key: string, val: string) => void;  
60 - onToggleFullscreen?: () => void; 64 + onModelChange?: onChangeFn;
  65 + onColorClick?: OnColorClickFn;
  66 + onToggleFullscreen?: onToggleFullscreenFn;
61 mapper?: {[type: string]: any}; 67 mapper?: {[type: string]: any};
62 } 68 }
63 69
@@ -98,22 +104,24 @@ export interface JsonFormData { @@ -98,22 +104,24 @@ export interface JsonFormData {
98 [key: string]: any; 104 [key: string]: any;
99 } 105 }
100 106
  107 +export type ComponentBuilderFn = (form: JsonFormData,
  108 + model: any,
  109 + index: number,
  110 + onChange: onChangeFn,
  111 + onColorClick: OnColorClickFn,
  112 + onToggleFullscreen: onToggleFullscreenFn,
  113 + mapper: {[type: string]: any}) => JSX.Element;
  114 +
101 export interface JsonFormFieldProps { 115 export interface JsonFormFieldProps {
102 value: any; 116 value: any;
103 model: any; 117 model: any;
104 form: JsonFormData; 118 form: JsonFormData;
105 - builder: (form: JsonFormData,  
106 - model: any,  
107 - index: number,  
108 - onChange: (key: (string | number)[], val: any) => void,  
109 - onColorClick: (event: MouseEvent, key: string, val: string) => void,  
110 - onToggleFullscreen: () => void,  
111 - mapper: {[type: string]: any}) => JSX.Element; 119 + builder: ComponentBuilderFn;
112 mapper?: {[type: string]: any}; 120 mapper?: {[type: string]: any};
113 - onChange?: (key: (string | number)[], val: any) => void;  
114 - onColorClick?: (event: MouseEvent, key: string, val: string) => void; 121 + onChange?: onChangeFn;
  122 + onColorClick?: OnColorClickFn;
115 onChangeValidate?: (e: any) => void; 123 onChangeValidate?: (e: any) => void;
116 - onToggleFullscreen?: () => void; 124 + onToggleFullscreen?: onToggleFullscreenFn;
117 valid?: boolean; 125 valid?: boolean;
118 error?: string; 126 error?: string;
119 options?: { 127 options?: {
@@ -21,47 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; @@ -21,47 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
21 $input-label-float-offset: 6px !default; 21 $input-label-float-offset: 6px !default;
22 $input-label-float-scale: .75 !default; 22 $input-label-float-scale: .75 !default;
23 23
24 -.tb-json-form {  
25 - &.tb-fullscreen {  
26 - [name="ReactSchemaForm"] {  
27 - .SchemaForm {  
28 - &.SchemaFormFullscreen {  
29 - position: absolute;  
30 - top: 0;  
31 - right: 0;  
32 - bottom: 0;  
33 - left: 0;  
34 -  
35 - > div:not(.fullscreen-form-field) {  
36 - display: none !important;  
37 - }  
38 -  
39 - > div.fullscreen-form-field {  
40 - position: relative;  
41 - width: 100%;  
42 - height: 100%;  
43 - }  
44 - }  
45 - } 24 +$previewSize: 100px !default;
46 25
47 - > div {  
48 - > section {  
49 - margin: 0 !important;  
50 - box-shadow: none !important;  
51 -  
52 - .SchemaGroupname {  
53 - display: none !important;  
54 - } 26 +.tb-json-form {
55 27
56 - > div {  
57 - padding: 0 !important;  
58 - }  
59 - }  
60 - } 28 + &.tb-fullscreen {
  29 + background: #fff;
  30 + position: absolute;
  31 + top: 0;
  32 + right: 0;
  33 + bottom: 0;
  34 + left: 0;
  35 + > div.fullscreen-form-field {
  36 + position: relative;
  37 + width: 100%;
  38 + height: 100%;
61 } 39 }
62 } 40 }
63 41
64 -  
65 .json-form-error { 42 .json-form-error {
66 position: relative; 43 position: relative;
67 bottom: -5px; 44 bottom: -5px;
@@ -74,6 +51,7 @@ $input-label-float-scale: .75 !default; @@ -74,6 +51,7 @@ $input-label-float-scale: .75 !default;
74 51
75 .tb-container { 52 .tb-container {
76 position: relative; 53 position: relative;
  54 + box-sizing: border-box;
77 padding: 10px 0; 55 padding: 10px 0;
78 margin-top: 32px; 56 margin-top: 32px;
79 } 57 }
@@ -144,6 +122,7 @@ $input-label-float-scale: .75 !default; @@ -144,6 +122,7 @@ $input-label-float-scale: .75 !default;
144 122
145 .tb-head-label { 123 .tb-head-label {
146 color: rgba(0, 0, 0, .54); 124 color: rgba(0, 0, 0, .54);
  125 + padding-bottom: 15px;
147 } 126 }
148 127
149 .SchemaGroupname { 128 .SchemaGroupname {
@@ -154,4 +133,130 @@ $input-label-float-scale: .75 !default; @@ -154,4 +133,130 @@ $input-label-float-scale: .75 !default;
154 .invisible { 133 .invisible {
155 display: none; 134 display: none;
156 } 135 }
  136 +
  137 + span.tb-toggle-icon {
  138 + padding-top: 12px;
  139 + padding-bottom: 12px;
  140 + }
  141 +
  142 + .tb-button-toggle .tb-toggle-icon {
  143 + display: inline-block;
  144 + width: 15px;
  145 + margin: auto 0 auto auto;
  146 + background-size: 100% auto;
  147 +
  148 + transition: transform .3s, ease-in-out;
  149 + }
  150 +
  151 + .tb-button-toggle .tb-toggle-icon.tb-toggled {
  152 + transform: rotateZ(180deg);
  153 + }
  154 +
  155 + .fullscreen-form-field {
  156 + .json-form-ace-editor {
  157 + height: calc(100% - 60px);
  158 + }
  159 + }
  160 +
  161 + .json-form-ace-editor {
  162 + position: relative;
  163 + height: 100%;
  164 + border: 1px solid #c0c0c0;
  165 +
  166 + .title-panel {
  167 + position: absolute;
  168 + top: 10px;
  169 + right: 20px;
  170 + z-index: 5;
  171 + font-size: .8rem;
  172 + font-weight: 500;
  173 +
  174 + label {
  175 + padding: 4px;
  176 + color: #00acc1;
  177 + text-transform: uppercase;
  178 + background: rgba(220, 220, 220, .35);
  179 + border-radius: 5px;
  180 + }
  181 +
  182 + button.tidy-button {
  183 + background: rgba(220, 220, 220, .35) !important;
  184 +
  185 + span {
  186 + padding: 0 !important;
  187 + font-size: 12px !important;
  188 + }
  189 + }
  190 + }
  191 + }
  192 +
  193 + .tb-image-select-container {
  194 + position: relative;
  195 + width: 100%;
  196 + height: $previewSize;
  197 + }
  198 +
  199 + .tb-image-preview {
  200 + width: auto;
  201 + max-width: $previewSize;
  202 + height: auto;
  203 + max-height: $previewSize;
  204 + }
  205 +
  206 + .tb-image-preview-container {
  207 + position: relative;
  208 + float: left;
  209 + width: $previewSize;
  210 + height: $previewSize;
  211 + margin-right: 12px;
  212 + vertical-align: top;
  213 + border: solid 1px;
  214 +
  215 + div {
  216 + width: 100%;
  217 + font-size: 18px;
  218 + text-align: center;
  219 + }
  220 +
  221 + div, .tb-image-preview {
  222 + position: absolute;
  223 + top: 50%;
  224 + left: 50%;
  225 + transform: translate(-50%, -50%);
  226 + }
  227 + }
  228 +
  229 + .tb-dropzone {
  230 + outline: none;
  231 + position: relative;
  232 + height: $previewSize;
  233 + padding: 0 8px;
  234 + overflow: hidden;
  235 + vertical-align: top;
  236 + border: dashed 2px;
  237 +
  238 + div {
  239 + position: absolute;
  240 + top: 50%;
  241 + left: 50%;
  242 + width: 100%;
  243 + font-size: 24px;
  244 + text-align: center;
  245 + transform: translate(-50%, -50%);
  246 + }
  247 + }
  248 +
  249 + .tb-image-clear-container {
  250 + position: relative;
  251 + float: right;
  252 + width: 48px;
  253 + height: $previewSize;
  254 + }
  255 +
  256 + .tb-image-clear-btn {
  257 + position: absolute !important;
  258 + top: 50%;
  259 + transform: translate(0%, -50%) !important;
  260 + }
  261 +
157 } 262 }
@@ -283,11 +283,6 @@ export interface LegendData { @@ -283,11 +283,6 @@ export interface LegendData {
283 data: Array<LegendKeyData>; 283 data: Array<LegendKeyData>;
284 } 284 }
285 285
286 -export interface WidgetConfigSettings {  
287 - [key: string]: any;  
288 - // TODO:  
289 -}  
290 -  
291 export enum WidgetActionType { 286 export enum WidgetActionType {
292 openDashboardState = 'openDashboardState', 287 openDashboardState = 'openDashboardState',
293 updateDashboardState = 'updateDashboardState', 288 updateDashboardState = 'updateDashboardState',
@@ -347,7 +342,7 @@ export interface WidgetConfig { @@ -347,7 +342,7 @@ export interface WidgetConfig {
347 units?: string; 342 units?: string;
348 decimals?: number; 343 decimals?: number;
349 actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; 344 actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
350 - settings?: WidgetConfigSettings; 345 + settings?: any;
351 alarmSource?: Datasource; 346 alarmSource?: Datasource;
352 alarmSearchStatus?: AlarmSearchStatus; 347 alarmSearchStatus?: AlarmSearchStatus;
353 alarmsPollingInterval?: number; 348 alarmsPollingInterval?: number;
@@ -39,3 +39,12 @@ @@ -39,3 +39,12 @@
39 margin: auto; 39 margin: auto;
40 } 40 }
41 } 41 }
  42 +
  43 +@mixin tb-checkered-bg() {
  44 + background-color: #fff;
  45 + background-image:
  46 + linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),
  47 + linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);
  48 + background-position: 0 0, 4px 4px;
  49 + background-size: 8px 8px;
  50 +}
@@ -852,4 +852,25 @@ mat-label { @@ -852,4 +852,25 @@ mat-label {
852 } 852 }
853 } 853 }
854 } 854 }
  855 +
  856 + .tb-color-preview {
  857 + cursor: pointer;
  858 + box-sizing: border-box;
  859 + position: relative;
  860 + width: 24px;
  861 + min-width: 24px;
  862 + height: 24px;
  863 + overflow: hidden;
  864 + content: "";
  865 + border: 2px solid #fff;
  866 + border-radius: 50%;
  867 + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084);
  868 +
  869 + @include tb-checkered-bg();
  870 +
  871 + .tb-color-result {
  872 + width: 100%;
  873 + height: 100%;
  874 + }
  875 + }
855 } 876 }