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 3767 }
3768 3768 },
3769 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 3774 "date-format": {
3775 3775 "version": "2.1.0",
... ... @@ -4164,9 +4164,9 @@
4164 4164 "dev": true
4165 4165 },
4166 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 4170 "dev": true
4171 4171 },
4172 4172 "elliptic": {
... ...
... ... @@ -41,7 +41,7 @@
41 41 "base64-js": "^1.3.1",
42 42 "compass-sass-mixins": "^0.12.7",
43 43 "core-js": "^3.1.4",
44   - "date-fns": "^2.5.0",
  44 + "date-fns": "2.1.0",
45 45 "deep-equal": "^1.0.1",
46 46 "flot": "git://github.com/thingsboard/flot.git#0.9-work",
47 47 "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",
... ...
... ... @@ -21,7 +21,7 @@
21 21 #entityAliasInput
22 22 formControlName="entityAlias"
23 23 (focusin)="onFocus()"
24   - [required]="required"
  24 + [required]="tbRequired"
25 25 (keydown)="entityAliasEnter($event)"
26 26 (keypress)="entityAliasEnter($event)"
27 27 [matAutocomplete]="entityAliasAutocomplete">
... ... @@ -54,7 +54,7 @@
54 54 </div>
55 55 </mat-option>
56 56 </mat-autocomplete>
57   - <mat-error *ngIf="!modelValue && required">
  57 + <mat-error *ngIf="!modelValue && tbRequired">
58 58 {{ 'entity.alias-required' | translate }}
59 59 </mat-error>
60 60 </mat-form-field>
... ...
... ... @@ -75,11 +75,11 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
75 75
76 76
77 77 private requiredValue: boolean;
78   - get required(): boolean {
  78 + get tbRequired(): boolean {
79 79 return this.requiredValue;
80 80 }
81 81 @Input()
82   - set required(value: boolean) {
  82 + set tbRequired(value: boolean) {
83 83 this.requiredValue = coerceBooleanProperty(value);
84 84 }
85 85
... ... @@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
151 151
152 152 isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
153 153 const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
154   - const customErrorState = this.required && !this.modelValue;
  154 + const customErrorState = this.tbRequired && !this.modelValue;
155 155 return originalErrorState || customErrorState;
156 156 }
157 157
... ...
... ... @@ -16,9 +16,8 @@
16 16
17 17 -->
18 18 <mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}"
19   - [formGroup]="dataKeyFormGroup"
20 19 class="tb-datakey-config">
21   - <mat-tab label="{{ 'datakey.settings' | translate }}">
  20 + <mat-tab [formGroup]="dataKeyFormGroup" label="{{ 'datakey.settings' | translate }}">
22 21 <div class="mat-content mat-padding" fxLayout="column">
23 22 <mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function">
24 23 <mat-label>{{ 'entity.key' | translate }}</mat-label>
... ... @@ -94,12 +93,10 @@
94 93 </section>
95 94 </div>
96 95 </mat-tab>
97   - <mat-tab label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced">
  96 + <mat-tab [formGroup]="dataKeySettingsFormGroup" label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced">
98 97 <div class="mat-content mat-padding" fxLayout="column">
99 98 <div style="overflow: auto;">
100 99 <tb-json-form
101   - [schema]="dataKeySchema"
102   - [form]="dataKeyForm"
103 100 formControlName="settings">
104 101 </tb-json-form>
105 102 </div>
... ...
... ... @@ -39,6 +39,7 @@ import { Observable, of } from 'rxjs';
39 39 import { map, mergeMap, tap } from 'rxjs/operators';
40 40 import { alarmFields } from '@shared/models/alarm.models';
41 41 import { JsFuncComponent } from '@shared/components/js-func.component';
  42 +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
42 43
43 44 @Component({
44 45 selector: 'tb-data-key-config',
... ... @@ -77,15 +78,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
77 78
78 79 displayAdvanced = false;
79 80
80   - dataKeySchema: any;
81   - dataKeyForm: any;
82   -
83 81 private modelValue: DataKey;
84 82
85 83 private propagateChange = null;
86 84
87 85 public dataKeyFormGroup: FormGroup;
88 86
  87 + public dataKeySettingsFormGroup: FormGroup;
  88 +
  89 + private dataKeySettingsData: JsonFormComponentData;
  90 +
89 91 private alarmKeys: Array<DataKey>;
90 92
91 93 filteredKeys: Observable<Array<string>>;
... ... @@ -112,8 +114,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
112 114 }
113 115 if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) {
114 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 128 this.dataKeyFormGroup = this.fb.group({
119 129 name: [null, []],
... ... @@ -123,8 +133,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
123 133 decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
124 134 funcBody: [null, []],
125 135 usePostProcessing: [null, []],
126   - postFuncBody: [null, []],
127   - settings: [null, []]
  136 + postFuncBody: [null, []]
128 137 });
129 138
130 139 this.dataKeyFormGroup.valueChanges.subscribe(() => {
... ... @@ -162,10 +171,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
162 171 this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
163 172 this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []);
164 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 182 private updateModel() {
168 183 this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value};
  184 + if (this.displayAdvanced) {
  185 + this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model;
  186 + }
169 187 this.propagateChange(this.modelValue);
170 188 }
171 189
... ... @@ -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 250 return null;
226 251 }
227 252 }
... ...
... ... @@ -14,15 +14,6 @@
14 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 17 :host {
27 18 .mat-chip.mat-standard-chip {
28 19 .tb-attribute-chip {
... ... @@ -56,27 +47,6 @@
56 47 color: inherit;
57 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 31 </mat-checkbox>
32 32 </div>
33 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 35 <tb-timewindow asButton="true"
36 36 aggregation="{{ widgetType === widgetTypes.timeseries }}"
37 37 fxFlex formControlName="timewindow"></tb-timewindow>
... ... @@ -64,12 +64,12 @@
64 64 <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
65 65 widgetType !== widgetTypes.alarm &&
66 66 widgetType !== widgetTypes.static &&
67   - isDataEnabled" [expanded]="true">
  67 + modelValue?.isDataEnabled" [expanded]="true">
68 68 <mat-expansion-panel-header>
69 69 <mat-panel-title fxLayout="column">
70 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 73 </mat-panel-title>
74 74 </mat-expansion-panel-header>
75 75 <div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate">
... ... @@ -90,7 +90,7 @@
90 90 <div style="overflow: auto; padding-bottom: 15px;">
91 91 <div fxFlex fxLayout="row" fxLayoutAlign="start center"
92 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 94 <span fxFlex="5">{{$index + 1}}.</span>
95 95 <div [formGroupName]="$index" class="mat-elevation-z4" fxFlex
96 96 fxLayout="row"
... ... @@ -120,7 +120,7 @@
120 120 </ng-template>
121 121 <ng-template [ngSwitchCase]="datasourceType.entity">
122 122 <tb-entity-alias-select
123   - [required]="datasourceControl.get('type').value === datasourceType.entity"
  123 + [tbRequired]="datasourceControl.get('type').value === datasourceType.entity"
124 124 [aliasController]="aliasController"
125 125 formControlName="entityAliasId"
126 126 [callbacks]="widgetConfigCallbacks">
... ... @@ -130,25 +130,15 @@
130 130 <tb-data-keys class="tb-data-keys" fxFlex
131 131 [widgetType]="widgetType"
132 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 135 [aliasController]="aliasController"
136   - [datakeySettingsSchema]="dataKeySettingsSchema"
  136 + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
137 137 [callbacks]="widgetConfigCallbacks"
138 138 [entityAliasId]="datasourceControl.get('entityAliasId').value"
139 139 formControlName="dataKeys">
140 140 </tb-data-keys>
141 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 142 <button [disabled]="isLoading$ | async"
153 143 mat-button mat-icon-button color="primary"
154 144 style="min-width: 40px;"
... ... @@ -164,8 +154,8 @@
164 154 <div fxFlex fxLayout="row" fxLayoutAlign="start center">
165 155 <button [disabled]="isLoading$ | async"
166 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 159 (click)="addDatasource()"
170 160 matTooltip="{{ 'widget-config.add-datasource' | translate }}"
171 161 matTooltipPosition="above">
... ... @@ -175,7 +165,7 @@
175 165 </div>
176 166 </mat-expansion-panel>
177 167 <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
178   - isDataEnabled" [expanded]="true">
  168 + modelValue?.isDataEnabled" [expanded]="true">
179 169 <mat-expansion-panel-header>
180 170 <mat-panel-title>
181 171 {{ 'widget-config.target-device' | translate }}
... ... @@ -183,7 +173,7 @@
183 173 </mat-expansion-panel-header>
184 174 <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
185 175 <tb-entity-alias-select fxFlex
186   - [required]="!widgetEditMode"
  176 + [tbRequired]="!widgetEditMode"
187 177 [aliasController]="aliasController"
188 178 [allowedEntityTypes]="[entityTypes.DEVICE]"
189 179 [callbacks]="widgetConfigCallbacks"
... ... @@ -193,6 +183,14 @@
193 183 </mat-expansion-panel>
194 184 </div>
195 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 195 </mat-tab>
198 196 </mat-tab-group>
... ...
... ... @@ -25,7 +25,6 @@ import {
25 25 LegendConfig,
26 26 WidgetActionDescriptor,
27 27 WidgetActionSource,
28   - WidgetConfigSettings,
29 28 widgetType,
30 29 WidgetTypeParameters
31 30 } from '@shared/models/widget.models';
... ... @@ -38,7 +37,7 @@ import {
38 37 FormGroup,
39 38 NG_VALIDATORS,
40 39 NG_VALUE_ACCESSOR,
41   - Validator,
  40 + Validator, ValidatorFn,
42 41 Validators
43 42 } from '@angular/forms';
44 43 import { WidgetConfigComponentData } from '@home/models/widget-component.models';
... ... @@ -59,6 +58,16 @@ import {
59 58 import { tap, mergeMap, map, catchError } from 'rxjs/operators';
60 59 import { MatDialog } from '@angular/material/dialog';
61 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 72 @Component({
64 73 selector: 'tb-widget-config',
... ... @@ -89,27 +98,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
89 98 forceExpandDatasources: boolean;
90 99
91 100 @Input()
92   - isDataEnabled: boolean;
93   -
94   - @Input()
95   - typeParameters: WidgetTypeParameters;
96   -
97   - @Input()
98   - actionSources: {[key: string]: WidgetActionSource};
99   -
100   - @Input()
101 101 aliasController: IAliasController;
102 102
103 103 @Input()
104 104 entityAliases: EntityAliases;
105 105
106 106 @Input()
107   - widgetSettingsSchema: any;
108   -
109   - @Input()
110   - dataKeySettingsSchema: any;
111   -
112   - @Input()
113 107 functionsOnly: boolean;
114 108
115 109 @Input() disabled: boolean;
... ... @@ -149,37 +143,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
149 143 legendConfig: LegendConfig;
150 144 actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
151 145 alarmSource: Datasource;
152   - settings: WidgetConfigSettings;
153 146 mobileOrder: number;
154 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 149 private modelValue: WidgetConfigComponentData;
175 150
176 151 private propagateChange = null;
177 152
178 153 public dataSettings: FormGroup;
179 154 public targetDeviceSettings: FormGroup;
  155 + public advancedSettings: FormGroup;
180 156
181 157 private dataSettingsChangesSubscription: Subscription;
182 158 private targetDeviceSettingsSubscription: Subscription;
  159 + private advancedSettingsSubscription: Subscription;
183 160
184 161 constructor(protected store: Store<AppState>,
185 162 private utils: UtilsService,
... ... @@ -207,6 +184,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
207 184 this.targetDeviceSettingsSubscription.unsubscribe();
208 185 this.targetDeviceSettingsSubscription = null;
209 186 }
  187 + if (this.advancedSettingsSubscription) {
  188 + this.advancedSettingsSubscription.unsubscribe();
  189 + this.advancedSettingsSubscription = null;
  190 + }
210 191 }
211 192
212 193 private createChangeSubscriptions() {
... ... @@ -216,11 +197,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
216 197 this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe(
217 198 () => this.updateTargetDeviceSettings()
218 199 );
  200 + this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe(
  201 + () => this.updateAdvancedSettings()
  202 + );
219 203 }
220 204
221 205 private buildForms() {
222 206 this.dataSettings = this.fb.group({});
223 207 this.targetDeviceSettings = this.fb.group({});
  208 + this.advancedSettings = this.fb.group({});
224 209 if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
225 210 this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
226 211 this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
... ... @@ -240,7 +225,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
240 225 [Validators.required, Validators.min(1)]));
241 226 }
242 227 }
243   - if (this.isDataEnabled) {
  228 + if (this.modelValue.isDataEnabled) {
244 229 if (this.widgetType !== widgetType.rpc &&
245 230 this.widgetType !== widgetType.alarm &&
246 231 this.widgetType !== widgetType.static) {
... ... @@ -252,6 +237,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
252 237 this.widgetEditMode ? [] : [Validators.required]));
253 238 }
254 239 }
  240 + this.advancedSettings.addControl('settings',
  241 + this.fb.control(null, []));
255 242 }
256 243
257 244 registerOnChange(fn: any): void {
... ... @@ -323,7 +310,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
323 310 { timewindow: config.timewindow }, {emitEvent: false}
324 311 );
325 312 }
326   - if (this.isDataEnabled) {
  313 + if (this.modelValue.isDataEnabled) {
327 314 if (this.widgetType !== widgetType.rpc &&
328 315 this.widgetType !== widgetType.alarm &&
329 316 this.widgetType !== widgetType.static) {
... ... @@ -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 359 if (layout) {
374 360 this.mobileOrder = layout.mobileOrder;
... ... @@ -383,7 +369,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
383 369 }
384 370
385 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 373 const datasourceFormGroup = this.fb.group(
388 374 {
389 375 type: [datasource ? datasource.type : null, [Validators.required]],
... ... @@ -402,18 +388,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
402 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 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 407 private updateDataSettings() {
... ... @@ -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 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 444 public removeDatasource(index: number) {
... ... @@ -450,7 +448,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
450 448 public addDatasource() {
451 449 let newDatasource: Datasource;
452 450 if (this.functionsOnly) {
453   - newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
  451 + newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema));
454 452 newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)];
455 453 } else {
456 454 newDatasource = { type: DatasourceType.entity,
... ... @@ -489,8 +487,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
489 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 493 return result;
496 494 }
... ... @@ -605,9 +603,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
605 603 valid: false
606 604 }
607 605 };
  606 + } else if (!this.advancedSettings.valid) {
  607 + return {
  608 + advancedSettings: {
  609 + valid: false
  610 + }
  611 + };
608 612 } else {
609 613 const config = this.modelValue.config;
610   - if (this.widgetType === widgetType.rpc && this.isDataEnabled) {
  614 + if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) {
611 615 if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) {
612 616 return {
613 617 targetDeviceAliasIds: {
... ... @@ -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 623 if (!config.alarmSource) {
620 624 return {
621 625 alarmSource: {
... ... @@ -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 631 if (!config.datasources || !config.datasources.length) {
628 632 return {
629 633 datasources: {
... ...
... ... @@ -22,7 +22,6 @@ import {
22 22 WidgetActionDescriptor,
23 23 WidgetActionSource,
24 24 WidgetConfig,
25   - WidgetConfigSettings,
26 25 WidgetControllerDescriptor,
27 26 WidgetType,
28 27 widgetType,
... ... @@ -74,7 +73,7 @@ export interface WidgetContext {
74 73 isMobile?: boolean;
75 74 dashboard?: IDashboardComponent;
76 75 widgetConfig?: WidgetConfig;
77   - settings?: WidgetConfigSettings;
  76 + settings?: any;
78 77 units?: string;
79 78 decimals?: number;
80 79 subscriptions?: {[id: string]: IWidgetSubscription};
... ... @@ -122,6 +121,11 @@ export interface WidgetConfigComponentData {
122 121 config: WidgetConfig;
123 122 layout: WidgetLayout;
124 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 131 export const MissingWidgetType: WidgetInfo = {
... ...
... ... @@ -172,7 +172,7 @@
172 172 [opened]="isEditingWidget"
173 173 mode="over"
174 174 position="end">
175   - <tb-details-panel fxFlex
  175 + <tb-details-panel *ngIf="isEditingWidget" fxFlex
176 176 headerTitle="{{editingWidget?.config.title}}"
177 177 headerSubtitle="{{ editingWidgetSubtitle }}"
178 178 [isReadOnly]="false"
... ... @@ -180,20 +180,17 @@
180 180 (closeDetails)="onEditWidgetClosed()"
181 181 (toggleDetailsEditMode)="onRevertWidgetEdit()"
182 182 (applyDetails)="saveWidget()"
183   - [theForm]="widgetForm">
  183 + [theForm]="tbEditWidget.widgetForm">
184 184 <div class="details-buttons">
185 185 <div [tb-help]="helpLinkIdForWidgetType()"></div>
186 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 194 </tb-details-panel>
198 195 </mat-drawer>
199 196 </mat-drawer-container>
... ...
... ... @@ -71,6 +71,7 @@ import {
71 71 EntityAliasesDialogData
72 72 } from '@home/components/alias/entity-aliases-dialog.component';
73 73 import { EntityAliases } from '@app/shared/models/alias.models';
  74 +import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component';
74 75
75 76 @Component({
76 77 selector: 'tb-dashboard-page',
... ... @@ -109,7 +110,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
109 110 editingWidgetLayoutOriginal: WidgetLayout = null;
110 111 editingWidgetSubtitle: string = null;
111 112 editingLayoutCtx: DashboardPageLayoutContext = null;
112   - editingWidgetFormGroup: FormGroup;
113 113
114 114 thingsboardVersion: string = env.tbVersion;
115 115
... ... @@ -188,6 +188,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
188 188 set rightLayoutOpened(rightLayoutOpened: boolean) {
189 189 }
190 190
  191 + @ViewChild('tbEditWidget', {static: false}) editWidgetComponent: EditWidgetComponent;
  192 +
191 193 constructor(protected store: Store<AppState>,
192 194 @Inject(WINDOW) private window: Window,
193 195 private breakpointObserver: BreakpointObserver,
... ... @@ -205,10 +207,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
205 207 private dialog: MatDialog) {
206 208 super(store);
207 209
208   - this.editingWidgetFormGroup = this.fb.group({
209   - widgetConfig: [null]
210   - });
211   -
212 210 this.rxSubscriptions.push(this.route.data.subscribe(
213 211 (data) => {
214 212 this.init(data);
... ... @@ -630,15 +628,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
630 628 }
631 629
632 630 onRevertWidgetEdit() {
633   - if (this.editingWidgetFormGroup.dirty) {
634   - this.editingWidgetFormGroup.markAsPristine();
  631 + if (this.editWidgetComponent.widgetFormGroup.dirty) {
  632 + this.editWidgetComponent.widgetFormGroup.markAsPristine();
635 633 this.editingWidget = deepClone(this.editingWidgetOriginal);
636 634 this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
637 635 }
638 636 }
639 637
640 638 saveWidget() {
641   - this.editingWidgetFormGroup.markAsPristine();
  639 + this.editWidgetComponent.widgetFormGroup.markAsPristine();
642 640 const widget = deepClone(this.editingWidget);
643 641 const widgetLayout = deepClone(this.editingWidgetLayout);
644 642 const id = this.editingWidgetOriginal.id;
... ...
... ... @@ -15,14 +15,9 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<form [formGroup]="widgetFormGroup">
  18 +<form #widgetForm="ngForm" [formGroup]="widgetFormGroup">
19 19 <fieldset [disabled]="isLoading$ | async">
20 20 <tb-widget-config
21   - [typeParameters]="typeParameters"
22   - [actionSources]="actionSources"
23   - [isDataEnabled]="isDataEnabled"
24   - [widgetSettingsSchema]="settingsSchema"
25   - [dataKeySettingsSchema]="dataKeySettingsSchema"
26 21 [aliasController]="aliasController"
27 22 [functionsOnly]="widgetEditMode"
28 23 [entityAliases]="dashboard.configuration.entityAliases"
... ...
... ... @@ -14,7 +14,7 @@
14 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 18 import { PageComponent } from '@shared/components/page.component';
19 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
... ... @@ -29,7 +29,7 @@ import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models
29 29 import { WidgetComponentService } from '@home/components/widget/widget-component.service';
30 30 import { WidgetConfigComponentData } from '../../models/widget-component.models';
31 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 33 import { EntityType } from '@shared/models/entity-type.models';
34 34 import { Observable, of } from 'rxjs';
35 35 import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
... ... @@ -66,21 +66,20 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
66 66 @Input()
67 67 widgetLayout: WidgetLayout;
68 68
69   - @Input()
  69 + @ViewChild('widgetForm', {static: true}) widgetForm: NgForm;
  70 +
70 71 widgetFormGroup: FormGroup;
71 72
72 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 75 constructor(protected store: Store<AppState>,
81 76 private dialog: MatDialog,
  77 + private fb: FormBuilder,
82 78 private widgetComponentService: WidgetComponentService) {
83 79 super(store);
  80 + this.widgetFormGroup = this.fb.group({
  81 + widgetConfig: [null]
  82 + });
84 83 }
85 84
86 85 ngOnInit(): void {
... ... @@ -104,27 +103,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
104 103
105 104 private loadWidgetConfig() {
106 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 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 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 133 this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
129 134 }
130 135
... ...
... ... @@ -13,37 +13,9 @@
13 13 * See the License for the specific language governing permissions and
14 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 17 :host {
26 18 .mat-form-field {
27 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 18 Directive,
19 19 ElementRef,
20 20 EventEmitter,
21   - Input,
22   - Output,
  21 + Input, OnChanges,
  22 + Output, SimpleChanges,
23 23 ViewContainerRef
24 24 } from '@angular/core';
25 25 import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
... ... @@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
29 29 @Directive({
30 30 selector: '[tb-fullscreen]'
31 31 })
32   -export class FullscreenDirective {
  32 +export class FullscreenDirective implements OnChanges {
33 33
34 34 fullscreenValue = false;
35 35
... ... @@ -37,16 +37,10 @@ export class FullscreenDirective {
37 37 private parentElement: HTMLElement;
38 38
39 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 45 @Output()
52 46 fullscreenChanged = new EventEmitter<boolean>();
... ... @@ -56,11 +50,30 @@ export class FullscreenDirective {
56 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 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 77 const position = this.overlay.position();
65 78 const config = new OverlayConfig({
66 79 hasBackdrop: false,
... ... @@ -73,19 +86,19 @@ export class FullscreenDirective {
73 86
74 87 this.overlayRef = this.overlay.create(config);
75 88 this.overlayRef.attach(new EmptyPortal());
76   - this.overlayRef.overlayElement.append( targetElement.nativeElement );
  89 + this.overlayRef.overlayElement.append( targetElement );
77 90 this.fullscreenChanged.emit(true);
78 91 }
79 92
80 93 exitFullscreen() {
81   - const targetElement = this.elementRef;
  94 + const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
82 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 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 102 this.elementRef.nativeElement.classList.remove('tb-fullscreen');
90 103 }
91 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 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 21 <div #reactRoot></div>
21   - <div>{{ model | json }}</div>
22 22 </div>
... ...
... ... @@ -40,6 +40,7 @@ import * as React from 'react';
40 40 import * as ReactDOM from 'react-dom';
41 41 import ReactSchemaForm from './react/json-form-react';
42 42 import JsonFormUtils from './react/json-form-utils';
  43 +import { JsonFormComponentData } from './json-form-component.models';
43 44
44 45 @Component({
45 46 selector: 'tb-json-form',
... ... @@ -64,12 +65,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
64 65 @ViewChild('reactRoot', {static: true})
65 66 reactRootElmRef: ElementRef<HTMLElement>;
66 67
67   - @Input() schema: any;
68   -
69   - @Input() form: any;
70   -
71   - @Input() groupInfoes: any[];
72   -
73 68 private readonlyValue: boolean;
74 69 get readonly(): boolean {
75 70 return this.readonlyValue;
... ... @@ -91,11 +86,18 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
91 86 onToggleFullscreen: this.onToggleFullscreen.bind(this)
92 87 };
93 88
  89 + data: JsonFormComponentData;
  90 +
94 91 model: any;
  92 + schema: any;
  93 + form: any;
  94 + groupInfoes: any[];
95 95
96 96 isModelValid = true;
97 97
98 98 isFullscreen = false;
  99 + targetFullscreenElement: HTMLElement;
  100 + fullscreenFinishFn: () => void;
99 101
100 102 private propagateChange = null;
101 103
... ... @@ -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 142 this.model = inspector.sanitize(this.schema, this.model).data;
134 143 this.updateAndRender();
135 144 this.isModelValid = this.validateModel();
136 145 if (!this.isModelValid) {
137 146 this.updateView();
138 147 }
139   - }
  148 +}
140 149
141 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 157 ngOnChanges(changes: SimpleChanges): void {
146 158 for (const propName of Object.keys(changes)) {
147 159 const change = changes[propName];
148 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 162 this.updateAndRender();
155 163 }
156 164 }
157 165 }
158 166 }
159 167
160   - onFullscreenChanged() {}
161   -
162 168 private onModelChange(key: (string | number)[], val: any) {
163 169 if (isString(val) && val === '') {
164 170 val = undefined;
165 171 }
166 172 if (JsonFormUtils.updateValue(key, this.model, val)) {
167 173 this.formProps.model = this.model;
  174 + this.isModelValid = this.validateModel();
168 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 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 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 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 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 210 this.formProps.model = deepClone(this.model);
198 211 this.renderReactSchemaForm();
199 212 }
200 213
201   - private renderReactSchemaForm() {
  214 + private renderReactSchemaForm(destroy: boolean = true) {
  215 + if (destroy) {
  216 + this.destroyReactSchemaForm();
  217 + }
202 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 17 import JsonFormUtils from './json-form-utils';
18 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 23 constructor(props) {
23 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 17 import JsonFormUtils from './json-form-utils';
18 18
19 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 25 import ThingsboardRcSelect from './json-form-rc-select';
26 26 import ThingsboardNumber from './json-form-number';
27 27 import ThingsboardText from './json-form-text';
28 28 import ThingsboardSelect from './json-form-select';
29 29 import ThingsboardRadios from './json-form-radios';
30 30 import ThingsboardDate from './json-form-date';
31   -/*import ThingsboardImage from './json-form-image.jsx';*/
  31 +import ThingsboardImage from './json-form-image';
32 32 import ThingsboardCheckbox from './json-form-checkbox';
33 33 import ThingsboardHelp from './json-form-help';
34 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 37 import _ from 'lodash';
  38 +import * as tinycolor from 'tinycolor2';
38 39
39 40 class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
40 41
... ... @@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
52 53 select: ThingsboardSelect,
53 54 radios: ThingsboardRadios,
54 55 date: ThingsboardDate,
55   - // image: ThingsboardImage,
  56 + image: ThingsboardImage,
56 57 checkbox: ThingsboardCheckbox,
57 58 help: ThingsboardHelp,
58 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 65 'rc-select': ThingsboardRcSelect,
65 66 fieldset: ThingsboardFieldSet
66 67 };
... ... @@ -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 92 builder(form: JsonFormData,
91 93 model: any,
92 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 97 onToggleFullscreen: () => void,
96 98 mapper: {[type: string]: any}): JSX.Element {
97 99 const type = form.type;
... ... @@ -173,9 +175,9 @@ class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps
173 175 }
174 176
175 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 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 181 onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
180 182 <div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div>
181 183 </section>);
... ...
... ... @@ -544,11 +544,13 @@ function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[])
544 544
545 545 const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => {
546 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 556 if (!ignoreArrays && $schema.items) {
... ...
... ... @@ -18,12 +18,13 @@ import { isUndefined, isDefined, isString } from '@app/core/utils';
18 18 import * as equal from 'deep-equal';
19 19 import ObjectPath from 'objectpath';
20 20 import * as React from 'react';
  21 +import * as tinycolor from 'tinycolor2';
21 22
22 23 export interface SchemaValidationResult {
23 24 valid: boolean;
24 25 error?: {
25 26 message?: string;
26   - }
  27 + };
27 28 }
28 29
29 30 export interface FormOption {
... ... @@ -47,6 +48,11 @@ export interface GroupInfo {
47 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 56 export interface JsonFormProps {
51 57 model?: any;
52 58 schema?: any;
... ... @@ -55,9 +61,9 @@ export interface JsonFormProps {
55 61 isFullscreen: boolean;
56 62 ignore?: {[key: string]: boolean};
57 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 67 mapper?: {[type: string]: any};
62 68 }
63 69
... ... @@ -98,22 +104,24 @@ export interface JsonFormData {
98 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 115 export interface JsonFormFieldProps {
102 116 value: any;
103 117 model: any;
104 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 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 123 onChangeValidate?: (e: any) => void;
116   - onToggleFullscreen?: () => void;
  124 + onToggleFullscreen?: onToggleFullscreenFn;
117 125 valid?: boolean;
118 126 error?: string;
119 127 options?: {
... ...
... ... @@ -21,47 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
21 21 $input-label-float-offset: 6px !default;
22 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 42 .json-form-error {
66 43 position: relative;
67 44 bottom: -5px;
... ... @@ -74,6 +51,7 @@ $input-label-float-scale: .75 !default;
74 51
75 52 .tb-container {
76 53 position: relative;
  54 + box-sizing: border-box;
77 55 padding: 10px 0;
78 56 margin-top: 32px;
79 57 }
... ... @@ -144,6 +122,7 @@ $input-label-float-scale: .75 !default;
144 122
145 123 .tb-head-label {
146 124 color: rgba(0, 0, 0, .54);
  125 + padding-bottom: 15px;
147 126 }
148 127
149 128 .SchemaGroupname {
... ... @@ -154,4 +133,130 @@ $input-label-float-scale: .75 !default;
154 133 .invisible {
155 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 283 data: Array<LegendKeyData>;
284 284 }
285 285
286   -export interface WidgetConfigSettings {
287   - [key: string]: any;
288   - // TODO:
289   -}
290   -
291 286 export enum WidgetActionType {
292 287 openDashboardState = 'openDashboardState',
293 288 updateDashboardState = 'updateDashboardState',
... ... @@ -347,7 +342,7 @@ export interface WidgetConfig {
347 342 units?: string;
348 343 decimals?: number;
349 344 actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
350   - settings?: WidgetConfigSettings;
  345 + settings?: any;
351 346 alarmSource?: Datasource;
352 347 alarmSearchStatus?: AlarmSearchStatus;
353 348 alarmsPollingInterval?: number;
... ...
... ... @@ -39,3 +39,12 @@
39 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 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 }
... ...