Browse Source

Implement React Schema Form.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
c317be2a65
  1. 12
      ui-ngx/package-lock.json
  2. 2
      ui-ngx/package.json
  3. 4
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  4. 6
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts
  5. 7
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  6. 39
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  7. 30
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss
  8. 46
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  9. 120
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  10. 8
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  11. 21
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  12. 14
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  13. 7
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html
  14. 57
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts
  15. 28
      ui-ngx/src/app/shared/components/color-input.component.scss
  16. 59
      ui-ngx/src/app/shared/components/fullscreen.directive.ts
  17. 23
      ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts
  18. 6
      ui-ngx/src/app/shared/components/json-form/json-form.component.html
  19. 82
      ui-ngx/src/app/shared/components/json-form/json-form.component.ts
  20. 184
      ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx
  21. 3
      ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx
  22. 184
      ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx
  23. 39
      ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx
  24. 39
      ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx
  25. 109
      ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx
  26. 38
      ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx
  27. 38
      ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx
  28. 44
      ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx
  29. 12
      ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts
  30. 36
      ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts
  31. 177
      ui-ngx/src/app/shared/components/json-form/react/json-form.scss
  32. 7
      ui-ngx/src/app/shared/models/widget.models.ts
  33. 9
      ui-ngx/src/scss/mixins.scss
  34. 21
      ui-ngx/src/styles.scss

12
ui-ngx/package-lock.json

@ -3767,9 +3767,9 @@
} }
}, },
"date-fns": { "date-fns": {
"version": "2.5.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.5.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.1.0.tgz",
"integrity": "sha512-I6Tkis01//nRcmvMQw/MRE1HAtcuA5Ie6jGPb8bJZJub7494LGOObqkV3ParnsSVviAjk5C8mNKDqYVBzCopWg==" "integrity": "sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA=="
}, },
"date-format": { "date-format": {
"version": "2.1.0", "version": "2.1.0",
@ -4164,9 +4164,9 @@
"dev": true "dev": true
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.284", "version": "1.3.285",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.284.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.285.tgz",
"integrity": "sha512-duOA4IWKH4R8ttiE8q/7xfg6eheRvMKlGqOOcGlDukdHEDJ26Wf7cMrCiK9Am11mswR6E/a23jXVA4UPDthTIw==", "integrity": "sha512-DYR9KW723sUbGK++DCmCmM95AbNXT4Q0tlCFMcYijFjayhuDqlGYR68OemlP8MJj0gjkwdeItIUfd0oLCgw+4A==",
"dev": true "dev": true
}, },
"elliptic": { "elliptic": {

2
ui-ngx/package.json

@ -41,7 +41,7 @@
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"compass-sass-mixins": "^0.12.7", "compass-sass-mixins": "^0.12.7",
"core-js": "^3.1.4", "core-js": "^3.1.4",
"date-fns": "^2.5.0", "date-fns": "2.1.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"flot": "git://github.com/thingsboard/flot.git#0.9-work", "flot": "git://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master",

4
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html

@ -21,7 +21,7 @@
#entityAliasInput #entityAliasInput
formControlName="entityAlias" formControlName="entityAlias"
(focusin)="onFocus()" (focusin)="onFocus()"
[required]="required" [required]="tbRequired"
(keydown)="entityAliasEnter($event)" (keydown)="entityAliasEnter($event)"
(keypress)="entityAliasEnter($event)" (keypress)="entityAliasEnter($event)"
[matAutocomplete]="entityAliasAutocomplete"> [matAutocomplete]="entityAliasAutocomplete">
@ -54,7 +54,7 @@
</div> </div>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="!modelValue && required"> <mat-error *ngIf="!modelValue && tbRequired">
{{ 'entity.alias-required' | translate }} {{ 'entity.alias-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

6
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts

@ -75,11 +75,11 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get tbRequired(): boolean {
return this.requiredValue; return this.requiredValue;
} }
@Input() @Input()
set required(value: boolean) { set tbRequired(value: boolean) {
this.requiredValue = coerceBooleanProperty(value); this.requiredValue = coerceBooleanProperty(value);
} }
@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.required && !this.modelValue; const customErrorState = this.tbRequired && !this.modelValue;
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }

7
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html

@ -16,9 +16,8 @@
--> -->
<mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}" <mat-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}"
[formGroup]="dataKeyFormGroup"
class="tb-datakey-config"> class="tb-datakey-config">
<mat-tab label="{{ 'datakey.settings' | translate }}"> <mat-tab [formGroup]="dataKeyFormGroup" label="{{ 'datakey.settings' | translate }}">
<div class="mat-content mat-padding" fxLayout="column"> <div class="mat-content mat-padding" fxLayout="column">
<mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function"> <mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function">
<mat-label>{{ 'entity.key' | translate }}</mat-label> <mat-label>{{ 'entity.key' | translate }}</mat-label>
@ -94,12 +93,10 @@
</section> </section>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced"> <mat-tab [formGroup]="dataKeySettingsFormGroup" label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced">
<div class="mat-content mat-padding" fxLayout="column"> <div class="mat-content mat-padding" fxLayout="column">
<div style="overflow: auto;"> <div style="overflow: auto;">
<tb-json-form <tb-json-form
[schema]="dataKeySchema"
[form]="dataKeyForm"
formControlName="settings"> formControlName="settings">
</tb-json-form> </tb-json-form>
</div> </div>

39
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts

@ -39,6 +39,7 @@ import { Observable, of } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators'; import { map, mergeMap, tap } from 'rxjs/operators';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
import { JsFuncComponent } from '@shared/components/js-func.component'; import { JsFuncComponent } from '@shared/components/js-func.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
@Component({ @Component({
selector: 'tb-data-key-config', selector: 'tb-data-key-config',
@ -77,15 +78,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
displayAdvanced = false; displayAdvanced = false;
dataKeySchema: any;
dataKeyForm: any;
private modelValue: DataKey; private modelValue: DataKey;
private propagateChange = null; private propagateChange = null;
public dataKeyFormGroup: FormGroup; public dataKeyFormGroup: FormGroup;
public dataKeySettingsFormGroup: FormGroup;
private dataKeySettingsData: JsonFormComponentData;
private alarmKeys: Array<DataKey>; private alarmKeys: Array<DataKey>;
filteredKeys: Observable<Array<string>>; filteredKeys: Observable<Array<string>>;
@ -112,8 +114,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) {
this.displayAdvanced = true; this.displayAdvanced = true;
this.dataKeySchema = this.dataKeySettingsSchema.schema; this.dataKeySettingsData = {
this.dataKeyForm = this.dataKeySettingsSchema.form || ['*']; schema: this.dataKeySettingsSchema.schema,
form: this.dataKeySettingsSchema.form || ['*']
};
this.dataKeySettingsFormGroup = this.fb.group({
settings: [null, []]
});
this.dataKeySettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
} }
this.dataKeyFormGroup = this.fb.group({ this.dataKeyFormGroup = this.fb.group({
name: [null, []], name: [null, []],
@ -123,8 +133,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]],
funcBody: [null, []], funcBody: [null, []],
usePostProcessing: [null, []], usePostProcessing: [null, []],
postFuncBody: [null, []], postFuncBody: [null, []]
settings: [null, []]
}); });
this.dataKeyFormGroup.valueChanges.subscribe(() => { this.dataKeyFormGroup.valueChanges.subscribe(() => {
@ -162,10 +171,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []); this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []);
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData
}, {emitEvent: false});
}
} }
private updateModel() { private updateModel() {
this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value};
if (this.displayAdvanced) {
this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model;
}
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
@ -222,6 +240,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
}; };
} }
if (this.displayAdvanced && !this.dataKeySettingsFormGroup.valid) {
return {
dataKeySettings: {
valid: false
}
};
}
return null; return null;
} }
} }

30
ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss

@ -14,15 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
@mixin tb-checkered-bg() {
background-color: #fff;
background-image:
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);
background-position: 0 0, 4px 4px;
background-size: 8px 8px;
}
:host { :host {
.mat-chip.mat-standard-chip { .mat-chip.mat-standard-chip {
.tb-attribute-chip { .tb-attribute-chip {
@ -56,27 +47,6 @@
color: inherit; color: inherit;
opacity: inherit; opacity: inherit;
} }
.tb-color-preview {
cursor: pointer;
box-sizing: border-box;
position: relative;
width: 24px;
min-width: 24px;
height: 24px;
overflow: hidden;
content: "";
border: 2px solid #fff;
border-radius: 50%;
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);
@include tb-checkered-bg();
.tb-color-result {
width: 100%;
height: 100%;
}
}
} }
} }
} }

46
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -31,7 +31,7 @@
</mat-checkbox> </mat-checkbox>
</div> </div>
<section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
<span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span> <span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true" <tb-timewindow asButton="true"
aggregation="{{ widgetType === widgetTypes.timeseries }}" aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex formControlName="timewindow"></tb-timewindow> fxFlex formControlName="timewindow"></tb-timewindow>
@ -64,12 +64,12 @@
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc && <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm && widgetType !== widgetTypes.alarm &&
widgetType !== widgetTypes.static && widgetType !== widgetTypes.static &&
isDataEnabled" [expanded]="true"> modelValue?.isDataEnabled" [expanded]="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title fxLayout="column"> <mat-panel-title fxLayout="column">
<div class="tb-panel-title" translate>widget-config.datasources</div> <div class="tb-panel-title" translate>widget-config.datasources</div>
<div *ngIf="typeParameters && typeParameters.maxDatasources > -1" <div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: typeParameters.maxDatasources} }}</div> class="tb-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate"> <div *ngIf="dataSettings.get('datasources').length === 0; else datasourcesTemplate">
@ -90,7 +90,7 @@
<div style="overflow: auto; padding-bottom: 15px;"> <div style="overflow: auto; padding-bottom: 15px;">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" <div fxFlex fxLayout="row" fxLayoutAlign="start center"
formArrayName="datasources" formArrayName="datasources"
*ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index"> *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index;">
<span fxFlex="5">{{$index + 1}}.</span> <span fxFlex="5">{{$index + 1}}.</span>
<div [formGroupName]="$index" class="mat-elevation-z4" fxFlex <div [formGroupName]="$index" class="mat-elevation-z4" fxFlex
fxLayout="row" fxLayout="row"
@ -120,7 +120,7 @@
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="datasourceType.entity"> <ng-template [ngSwitchCase]="datasourceType.entity">
<tb-entity-alias-select <tb-entity-alias-select
[required]="datasourceControl.get('type').value === datasourceType.entity" [tbRequired]="datasourceControl.get('type').value === datasourceType.entity"
[aliasController]="aliasController" [aliasController]="aliasController"
formControlName="entityAliasId" formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks"> [callbacks]="widgetConfigCallbacks">
@ -130,25 +130,15 @@
<tb-data-keys class="tb-data-keys" fxFlex <tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType" [widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value" [datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="typeParameters?.maxDataKeys" [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="typeParameters?.dataKeysOptional" [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[aliasController]="aliasController" [aliasController]="aliasController"
[datakeySettingsSchema]="dataKeySettingsSchema" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[callbacks]="widgetConfigCallbacks" [callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value" [entityAliasId]="datasourceControl.get('entityAliasId').value"
formControlName="dataKeys"> formControlName="dataKeys">
</tb-data-keys> </tb-data-keys>
</section> </section>
<!--tb-datasource fxFlex
[widgetType]="widgetType"
[maxDataKeys]="typeParameters?.maxDataKeys"
[optDataKeys]="typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[functionsOnly]="functionsOnly"
[datakeySettingsSchema]="dataKeySettingsSchema"
[callbacks]="widgetConfigCallbacks"
[formControl]="datasourceControl"
></tb-datasource-->
<button [disabled]="isLoading$ | async" <button [disabled]="isLoading$ | async"
mat-button mat-icon-button color="primary" mat-button mat-icon-button color="primary"
style="min-width: 40px;" style="min-width: 40px;"
@ -164,8 +154,8 @@
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <div fxFlex fxLayout="row" fxLayoutAlign="start center">
<button [disabled]="isLoading$ | async" <button [disabled]="isLoading$ | async"
mat-button mat-raised-button color="primary" mat-button mat-raised-button color="primary"
[fxShow]="typeParameters && [fxShow]="modelValue?.typeParameters &&
(typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < typeParameters.maxDatasources)" (modelValue?.typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < modelValue?.typeParameters.maxDatasources)"
(click)="addDatasource()" (click)="addDatasource()"
matTooltip="{{ 'widget-config.add-datasource' | translate }}" matTooltip="{{ 'widget-config.add-datasource' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
@ -175,7 +165,7 @@
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc && <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
isDataEnabled" [expanded]="true"> modelValue?.isDataEnabled" [expanded]="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
{{ 'widget-config.target-device' | translate }} {{ 'widget-config.target-device' | translate }}
@ -183,7 +173,7 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div [formGroup]="targetDeviceSettings" style="padding: 0 5px;"> <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
<tb-entity-alias-select fxFlex <tb-entity-alias-select fxFlex
[required]="!widgetEditMode" [tbRequired]="!widgetEditMode"
[aliasController]="aliasController" [aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]" [allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks" [callbacks]="widgetConfigCallbacks"
@ -193,6 +183,14 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget-config.settings' | translate }}"> <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
<div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config"
fxLayout="column">
<tb-json-form
formControlName="settings">
</tb-json-form>
</div>
</mat-tab>
<mat-tab label="{{ 'widget-config.actions' | translate }}">
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>

120
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -25,7 +25,6 @@ import {
LegendConfig, LegendConfig,
WidgetActionDescriptor, WidgetActionDescriptor,
WidgetActionSource, WidgetActionSource,
WidgetConfigSettings,
widgetType, widgetType,
WidgetTypeParameters WidgetTypeParameters
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
@ -38,7 +37,7 @@ import {
FormGroup, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
Validator, Validator, ValidatorFn,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
@ -59,6 +58,16 @@ import {
import { tap, mergeMap, map, catchError } from 'rxjs/operators'; import { tap, mergeMap, map, catchError } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
const emptySettingsSchema = {
type: 'object',
properties: {}
};
const emptySettingsGroupInfoes = [];
const defaultSettingsForm = [
'*'
];
@Component({ @Component({
selector: 'tb-widget-config', selector: 'tb-widget-config',
@ -88,27 +97,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
@Input() @Input()
forceExpandDatasources: boolean; forceExpandDatasources: boolean;
@Input()
isDataEnabled: boolean;
@Input()
typeParameters: WidgetTypeParameters;
@Input()
actionSources: {[key: string]: WidgetActionSource};
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@Input() @Input()
entityAliases: EntityAliases; entityAliases: EntityAliases;
@Input()
widgetSettingsSchema: any;
@Input()
dataKeySettingsSchema: any;
@Input() @Input()
functionsOnly: boolean; functionsOnly: boolean;
@ -149,37 +143,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
legendConfig: LegendConfig; legendConfig: LegendConfig;
actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
alarmSource: Datasource; alarmSource: Datasource;
settings: WidgetConfigSettings;
mobileOrder: number; mobileOrder: number;
mobileHeight: number; mobileHeight: number;
emptySettingsSchema = {
type: 'object',
properties: {}
};
emptySettingsGroupInfoes = [];
defaultSettingsForm = [
'*'
];
currentSettingsSchema = deepClone(this.emptySettingsSchema);
currentSettings: WidgetConfigSettings = {};
currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);
currentSettingsForm: any;
private modelValue: WidgetConfigComponentData; private modelValue: WidgetConfigComponentData;
private propagateChange = null; private propagateChange = null;
public dataSettings: FormGroup; public dataSettings: FormGroup;
public targetDeviceSettings: FormGroup; public targetDeviceSettings: FormGroup;
public advancedSettings: FormGroup;
private dataSettingsChangesSubscription: Subscription; private dataSettingsChangesSubscription: Subscription;
private targetDeviceSettingsSubscription: Subscription; private targetDeviceSettingsSubscription: Subscription;
private advancedSettingsSubscription: Subscription;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private utils: UtilsService, private utils: UtilsService,
@ -207,6 +184,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettingsSubscription.unsubscribe(); this.targetDeviceSettingsSubscription.unsubscribe();
this.targetDeviceSettingsSubscription = null; this.targetDeviceSettingsSubscription = null;
} }
if (this.advancedSettingsSubscription) {
this.advancedSettingsSubscription.unsubscribe();
this.advancedSettingsSubscription = null;
}
} }
private createChangeSubscriptions() { private createChangeSubscriptions() {
@ -216,11 +197,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe(
() => this.updateTargetDeviceSettings() () => this.updateTargetDeviceSettings()
); );
this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe(
() => this.updateAdvancedSettings()
);
} }
private buildForms() { private buildForms() {
this.dataSettings = this.fb.group({}); this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
@ -240,7 +225,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
[Validators.required, Validators.min(1)])); [Validators.required, Validators.min(1)]));
} }
} }
if (this.isDataEnabled) { if (this.modelValue.isDataEnabled) {
if (this.widgetType !== widgetType.rpc && if (this.widgetType !== widgetType.rpc &&
this.widgetType !== widgetType.alarm && this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) { this.widgetType !== widgetType.static) {
@ -252,6 +237,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.widgetEditMode ? [] : [Validators.required])); this.widgetEditMode ? [] : [Validators.required]));
} }
} }
this.advancedSettings.addControl('settings',
this.fb.control(null, []));
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -323,7 +310,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
{ timewindow: config.timewindow }, {emitEvent: false} { timewindow: config.timewindow }, {emitEvent: false}
); );
} }
if (this.isDataEnabled) { if (this.modelValue.isDataEnabled) {
if (this.widgetType !== widgetType.rpc && if (this.widgetType !== widgetType.rpc &&
this.widgetType !== widgetType.alarm && this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) { this.widgetType !== widgetType.static) {
@ -366,9 +353,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
} }
} }
this.settings = config.settings;
this.updateSchemaForm(); this.updateSchemaForm(config.settings);
if (layout) { if (layout) {
this.mobileOrder = layout.mobileOrder; this.mobileOrder = layout.mobileOrder;
@ -383,7 +369,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
private buildDatasourceForm(datasource?: Datasource): AbstractControl { private buildDatasourceForm(datasource?: Datasource): AbstractControl {
const dataKeysRequired = !this.typeParameters || !this.typeParameters.dataKeysOptional; const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional;
const datasourceFormGroup = this.fb.group( const datasourceFormGroup = this.fb.group(
{ {
type: [datasource ? datasource.type : null, [Validators.required]], type: [datasource ? datasource.type : null, [Validators.required]],
@ -402,18 +388,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
return datasourceFormGroup; return datasourceFormGroup;
} }
private updateSchemaForm() { private updateSchemaForm(settings?: any) {
if (this.widgetSettingsSchema && this.widgetSettingsSchema.schema) { const widgetSettingsFormData: JsonFormComponentData = {};
this.currentSettingsSchema = this.widgetSettingsSchema.schema; if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {
this.currentSettingsForm = this.widgetSettingsSchema.form || deepClone(this.defaultSettingsForm); widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema;
this.currentSettingsGroupInfoes = this.widgetSettingsSchema.groupInfoes; widgetSettingsFormData.form = this.modelValue.settingsSchema.form || deepClone(defaultSettingsForm);
this.currentSettings = this.settings; widgetSettingsFormData.groupInfoes = this.modelValue.settingsSchema.groupInfoes;
widgetSettingsFormData.model = settings;
} else { } else {
this.currentSettingsForm = deepClone(this.defaultSettingsForm); widgetSettingsFormData.schema = deepClone(emptySettingsSchema);
this.currentSettingsSchema = deepClone(this.emptySettingsSchema); widgetSettingsFormData.form = deepClone(defaultSettingsForm);
this.currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes); widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes);
this.currentSettings = {}; widgetSettingsFormData.model = {};
} }
this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
} }
private updateDataSettings() { private updateDataSettings() {
@ -439,8 +427,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
} }
private updateAdvancedSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
const settings = this.advancedSettings.get('settings').value.model;
this.modelValue.config.settings = settings;
}
this.propagateChange(this.modelValue);
}
}
public displayAdvanced(): boolean { public displayAdvanced(): boolean {
return this.widgetSettingsSchema && this.widgetSettingsSchema.schema; return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema;
} }
public removeDatasource(index: number) { public removeDatasource(index: number) {
@ -450,7 +448,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
public addDatasource() { public addDatasource() {
let newDatasource: Datasource; let newDatasource: Datasource;
if (this.functionsOnly) { if (this.functionsOnly) {
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)]; newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)];
} else { } else {
newDatasource = { type: DatasourceType.entity, newDatasource = { type: DatasourceType.entity,
@ -489,8 +487,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
result.funcBody = 'return prevValue + 1;'; result.funcBody = 'return prevValue + 1;';
} }
} }
if (isDefined(this.dataKeySettingsSchema.schema)) { if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(this.dataKeySettingsSchema.schema); result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema);
} }
return result; return result;
} }
@ -605,9 +603,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
valid: false valid: false
} }
}; };
} else if (!this.advancedSettings.valid) {
return {
advancedSettings: {
valid: false
}
};
} else { } else {
const config = this.modelValue.config; const config = this.modelValue.config;
if (this.widgetType === widgetType.rpc && this.isDataEnabled) { if (this.widgetType === widgetType.rpc && this.modelValue.isDataEnabled) {
if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) { if (!config.targetDeviceAliasIds || !config.targetDeviceAliasIds.length) {
return { return {
targetDeviceAliasIds: { targetDeviceAliasIds: {
@ -615,7 +619,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
}; };
} }
} else if (this.widgetType === widgetType.alarm && this.isDataEnabled) { } else if (this.widgetType === widgetType.alarm && this.modelValue.isDataEnabled) {
if (!config.alarmSource) { if (!config.alarmSource) {
return { return {
alarmSource: { alarmSource: {
@ -623,7 +627,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
}; };
} }
} else if (this.widgetType !== widgetType.static && this.isDataEnabled) { } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) {
if (!config.datasources || !config.datasources.length) { if (!config.datasources || !config.datasources.length) {
return { return {
datasources: { datasources: {

8
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -22,7 +22,6 @@ import {
WidgetActionDescriptor, WidgetActionDescriptor,
WidgetActionSource, WidgetActionSource,
WidgetConfig, WidgetConfig,
WidgetConfigSettings,
WidgetControllerDescriptor, WidgetControllerDescriptor,
WidgetType, WidgetType,
widgetType, widgetType,
@ -74,7 +73,7 @@ export interface WidgetContext {
isMobile?: boolean; isMobile?: boolean;
dashboard?: IDashboardComponent; dashboard?: IDashboardComponent;
widgetConfig?: WidgetConfig; widgetConfig?: WidgetConfig;
settings?: WidgetConfigSettings; settings?: any;
units?: string; units?: string;
decimals?: number; decimals?: number;
subscriptions?: {[id: string]: IWidgetSubscription}; subscriptions?: {[id: string]: IWidgetSubscription};
@ -122,6 +121,11 @@ export interface WidgetConfigComponentData {
config: WidgetConfig; config: WidgetConfig;
layout: WidgetLayout; layout: WidgetLayout;
widgetType: widgetType; widgetType: widgetType;
typeParameters: WidgetTypeParameters;
actionSources: {[key: string]: WidgetActionSource};
isDataEnabled: boolean;
settingsSchema: any;
dataKeySettingsSchema: any;
} }
export const MissingWidgetType: WidgetInfo = { export const MissingWidgetType: WidgetInfo = {

21
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -172,7 +172,7 @@
[opened]="isEditingWidget" [opened]="isEditingWidget"
mode="over" mode="over"
position="end"> position="end">
<tb-details-panel fxFlex <tb-details-panel *ngIf="isEditingWidget" fxFlex
headerTitle="{{editingWidget?.config.title}}" headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}" headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="false" [isReadOnly]="false"
@ -180,20 +180,17 @@
(closeDetails)="onEditWidgetClosed()" (closeDetails)="onEditWidgetClosed()"
(toggleDetailsEditMode)="onRevertWidgetEdit()" (toggleDetailsEditMode)="onRevertWidgetEdit()"
(applyDetails)="saveWidget()" (applyDetails)="saveWidget()"
[theForm]="widgetForm"> [theForm]="tbEditWidget.widgetForm">
<div class="details-buttons"> <div class="details-buttons">
<div [tb-help]="helpLinkIdForWidgetType()"></div> <div [tb-help]="helpLinkIdForWidgetType()"></div>
</div> </div>
<form #widgetForm="ngForm" [formGroup]="editingWidgetFormGroup"> <tb-edit-widget #tbEditWidget
<tb-edit-widget *ngIf="isEditingWidget" [dashboard]="dashboard"
[dashboard]="dashboard" [aliasController]="dashboardCtx.aliasController"
[aliasController]="dashboardCtx.aliasController" [widgetEditMode]="widgetEditMode"
[widgetEditMode]="widgetEditMode" [widget]="editingWidget"
[widget]="editingWidget" [widgetLayout]="editingWidgetLayout">
[widgetLayout]="editingWidgetLayout" </tb-edit-widget>
[widgetFormGroup]="editingWidgetFormGroup">
</tb-edit-widget>
</form>
</tb-details-panel> </tb-details-panel>
</mat-drawer> </mat-drawer>
</mat-drawer-container> </mat-drawer-container>

14
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -71,6 +71,7 @@ import {
EntityAliasesDialogData EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component'; } from '@home/components/alias/entity-aliases-dialog.component';
import { EntityAliases } from '@app/shared/models/alias.models'; import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component';
@Component({ @Component({
selector: 'tb-dashboard-page', selector: 'tb-dashboard-page',
@ -109,7 +110,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
editingWidgetLayoutOriginal: WidgetLayout = null; editingWidgetLayoutOriginal: WidgetLayout = null;
editingWidgetSubtitle: string = null; editingWidgetSubtitle: string = null;
editingLayoutCtx: DashboardPageLayoutContext = null; editingLayoutCtx: DashboardPageLayoutContext = null;
editingWidgetFormGroup: FormGroup;
thingsboardVersion: string = env.tbVersion; thingsboardVersion: string = env.tbVersion;
@ -188,6 +188,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
set rightLayoutOpened(rightLayoutOpened: boolean) { set rightLayoutOpened(rightLayoutOpened: boolean) {
} }
@ViewChild('tbEditWidget', {static: false}) editWidgetComponent: EditWidgetComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
@ -205,10 +207,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private dialog: MatDialog) { private dialog: MatDialog) {
super(store); super(store);
this.editingWidgetFormGroup = this.fb.group({
widgetConfig: [null]
});
this.rxSubscriptions.push(this.route.data.subscribe( this.rxSubscriptions.push(this.route.data.subscribe(
(data) => { (data) => {
this.init(data); this.init(data);
@ -630,15 +628,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
onRevertWidgetEdit() { onRevertWidgetEdit() {
if (this.editingWidgetFormGroup.dirty) { if (this.editWidgetComponent.widgetFormGroup.dirty) {
this.editingWidgetFormGroup.markAsPristine(); this.editWidgetComponent.widgetFormGroup.markAsPristine();
this.editingWidget = deepClone(this.editingWidgetOriginal); this.editingWidget = deepClone(this.editingWidgetOriginal);
this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal); this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
} }
} }
saveWidget() { saveWidget() {
this.editingWidgetFormGroup.markAsPristine(); this.editWidgetComponent.widgetFormGroup.markAsPristine();
const widget = deepClone(this.editingWidget); const widget = deepClone(this.editingWidget);
const widgetLayout = deepClone(this.editingWidgetLayout); const widgetLayout = deepClone(this.editingWidgetLayout);
const id = this.editingWidgetOriginal.id; const id = this.editingWidgetOriginal.id;

7
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html

@ -15,14 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="widgetFormGroup"> <form #widgetForm="ngForm" [formGroup]="widgetFormGroup">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<tb-widget-config <tb-widget-config
[typeParameters]="typeParameters"
[actionSources]="actionSources"
[isDataEnabled]="isDataEnabled"
[widgetSettingsSchema]="settingsSchema"
[dataKeySettingsSchema]="dataKeySettingsSchema"
[aliasController]="aliasController" [aliasController]="aliasController"
[functionsOnly]="widgetEditMode" [functionsOnly]="widgetEditMode"
[entityAliases]="dashboard.configuration.entityAliases" [entityAliases]="dashboard.configuration.entityAliases"

57
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -29,7 +29,7 @@ import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models
import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models'; import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { deepClone, isDefined, isString } from '@core/utils'; import { deepClone, isDefined, isString } from '@core/utils';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
@ -66,21 +66,20 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input() @Input()
widgetLayout: WidgetLayout; widgetLayout: WidgetLayout;
@Input() @ViewChild('widgetForm', {static: true}) widgetForm: NgForm;
widgetFormGroup: FormGroup; widgetFormGroup: FormGroup;
widgetConfig: WidgetConfigComponentData; widgetConfig: WidgetConfigComponentData;
typeParameters: WidgetTypeParameters;
actionSources: {[key: string]: WidgetActionSource};
isDataEnabled: boolean;
settingsSchema: any;
dataKeySettingsSchema: any;
functionsOnly: boolean;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialog: MatDialog, private dialog: MatDialog,
private fb: FormBuilder,
private widgetComponentService: WidgetComponentService) { private widgetComponentService: WidgetComponentService) {
super(store); super(store);
this.widgetFormGroup = this.fb.group({
widgetConfig: [null]
});
} }
ngOnInit(): void { ngOnInit(): void {
@ -104,27 +103,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
private loadWidgetConfig() { private loadWidgetConfig() {
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget); const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
this.widgetConfig = { const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
config: this.widget.config, const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
layout: this.widgetLayout, const typeParameters = widgetInfo.typeParameters;
widgetType: this.widget.type const actionSources = widgetInfo.actionSources;
}; const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; let settingsSchema;
const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; if (!rawSettingsSchema || rawSettingsSchema === '') {
this.typeParameters = widgetInfo.typeParameters; settingsSchema = {};
this.actionSources = widgetInfo.actionSources;
this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
if (!settingsSchema || settingsSchema === '') {
this.settingsSchema = {};
} else { } else {
this.settingsSchema = isString(settingsSchema) ? JSON.parse(settingsSchema) : settingsSchema; settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema;
} }
if (!dataKeySettingsSchema || dataKeySettingsSchema === '') { let dataKeySettingsSchema;
this.dataKeySettingsSchema = {}; if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') {
dataKeySettingsSchema = {};
} else { } else {
this.dataKeySettingsSchema = isString(dataKeySettingsSchema) ? JSON.parse(dataKeySettingsSchema) : dataKeySettingsSchema; dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
} }
this.functionsOnly = this.dashboard ? false : true; this.widgetConfig = {
config: this.widget.config,
layout: this.widgetLayout,
widgetType: this.widget.type,
typeParameters,
actionSources,
isDataEnabled,
settingsSchema,
dataKeySettingsSchema
};
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
} }

28
ui-ngx/src/app/shared/components/color-input.component.scss

@ -13,37 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@mixin tb-checkered-bg() {
background-color: #fff;
background-image:
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);
background-position: 0 0, 4px 4px;
background-size: 8px 8px;
}
:host { :host {
.mat-form-field { .mat-form-field {
width: 100%; width: 100%;
} }
.tb-color-preview {
cursor: pointer;
box-sizing: border-box;
position: relative;
width: 24px;
min-width: 24px;
height: 24px;
overflow: hidden;
content: "";
border: 2px solid #fff;
border-radius: 50%;
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);
@include tb-checkered-bg();
.tb-color-result {
width: 100%;
height: 100%;
}
}
} }

59
ui-ngx/src/app/shared/components/fullscreen.directive.ts

@ -18,8 +18,8 @@ import {
Directive, Directive,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input, OnChanges,
Output, Output, SimpleChanges,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
@Directive({ @Directive({
selector: '[tb-fullscreen]' selector: '[tb-fullscreen]'
}) })
export class FullscreenDirective { export class FullscreenDirective implements OnChanges {
fullscreenValue = false; fullscreenValue = false;
@ -37,16 +37,10 @@ export class FullscreenDirective {
private parentElement: HTMLElement; private parentElement: HTMLElement;
@Input() @Input()
set fullscreen(fullscreen: boolean) { fullscreen: boolean;
if (this.fullscreenValue !== fullscreen) {
this.fullscreenValue = fullscreen; @Input()
if (this.fullscreenValue) { fullscreenElement: HTMLElement;
this.enterFullscreen();
} else {
this.exitFullscreen();
}
}
}
@Output() @Output()
fullscreenChanged = new EventEmitter<boolean>(); fullscreenChanged = new EventEmitter<boolean>();
@ -56,11 +50,30 @@ export class FullscreenDirective {
private overlay: Overlay) { private overlay: Overlay) {
} }
ngOnChanges(changes: SimpleChanges): void {
let updateFullscreen = false;
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'fullscreen') {
updateFullscreen = true;
}
}
}
if (updateFullscreen) {
if (this.fullscreen) {
this.enterFullscreen();
} else {
this.exitFullscreen();
}
}
}
enterFullscreen() { enterFullscreen() {
const targetElement = this.elementRef; const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
this.parentElement = targetElement.nativeElement.parentElement; this.parentElement = targetElement.parentElement;
this.parentElement.removeChild(targetElement.nativeElement); this.parentElement.removeChild(targetElement);
targetElement.nativeElement.classList.add('tb-fullscreen'); targetElement.classList.add('tb-fullscreen');
const position = this.overlay.position(); const position = this.overlay.position();
const config = new OverlayConfig({ const config = new OverlayConfig({
hasBackdrop: false, hasBackdrop: false,
@ -73,19 +86,19 @@ export class FullscreenDirective {
this.overlayRef = this.overlay.create(config); this.overlayRef = this.overlay.create(config);
this.overlayRef.attach(new EmptyPortal()); this.overlayRef.attach(new EmptyPortal());
this.overlayRef.overlayElement.append( targetElement.nativeElement ); this.overlayRef.overlayElement.append( targetElement );
this.fullscreenChanged.emit(true); this.fullscreenChanged.emit(true);
} }
exitFullscreen() { exitFullscreen() {
const targetElement = this.elementRef; const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
if (this.parentElement) { if (this.parentElement) {
this.overlayRef.overlayElement.removeChild( targetElement.nativeElement ); this.overlayRef.overlayElement.removeChild( targetElement );
this.parentElement.append(targetElement.nativeElement); this.parentElement.append(targetElement);
this.parentElement = null; this.parentElement = null;
} }
targetElement.nativeElement.classList.remove('tb-fullscreen'); targetElement.classList.remove('tb-fullscreen');
if (this.elementRef !== targetElement) { if (this.elementRef) {
this.elementRef.nativeElement.classList.remove('tb-fullscreen'); this.elementRef.nativeElement.classList.remove('tb-fullscreen');
} }
this.overlayRef.dispose(); this.overlayRef.dispose();

23
ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts

@ -0,0 +1,23 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export interface JsonFormComponentData {
model?: any;
schema?: any;
form?: any;
groupInfoes?: any[];
}

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

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreen]="isFullscreen" <div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreenElement]="targetFullscreenElement"
(fullscreenChanged)="onFullscreenChanged()"> [fullscreen]="isFullscreen"
(fullscreenChanged)="onFullscreenChanged($event)">
<div #reactRoot></div> <div #reactRoot></div>
<div>{{ model | json }}</div>
</div> </div>

82
ui-ngx/src/app/shared/components/json-form/json-form.component.ts

@ -40,6 +40,7 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import ReactSchemaForm from './react/json-form-react'; import ReactSchemaForm from './react/json-form-react';
import JsonFormUtils from './react/json-form-utils'; import JsonFormUtils from './react/json-form-utils';
import { JsonFormComponentData } from './json-form-component.models';
@Component({ @Component({
selector: 'tb-json-form', selector: 'tb-json-form',
@ -64,12 +65,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
@ViewChild('reactRoot', {static: true}) @ViewChild('reactRoot', {static: true})
reactRootElmRef: ElementRef<HTMLElement>; reactRootElmRef: ElementRef<HTMLElement>;
@Input() schema: any;
@Input() form: any;
@Input() groupInfoes: any[];
private readonlyValue: boolean; private readonlyValue: boolean;
get readonly(): boolean { get readonly(): boolean {
return this.readonlyValue; return this.readonlyValue;
@ -91,11 +86,18 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
onToggleFullscreen: this.onToggleFullscreen.bind(this) onToggleFullscreen: this.onToggleFullscreen.bind(this)
}; };
data: JsonFormComponentData;
model: any; model: any;
schema: any;
form: any;
groupInfoes: any[];
isModelValid = true; isModelValid = true;
isFullscreen = false; isFullscreen = false;
targetFullscreenElement: HTMLElement;
fullscreenFinishFn: () => void;
private propagateChange = null; private propagateChange = null;
@ -128,77 +130,91 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
}; };
} }
writeValue(value: any): void { writeValue(data: JsonFormComponentData): void {
this.model = value || {}; this.data = data;
this.schema = this.data && this.data.schema ? deepClone(this.data.schema) : {
type: 'object'
};
this.schema.strict = true;
this.form = this.data && this.data.form ? deepClone(this.data.form) : [ '*' ];
this.groupInfoes = this.data && this.data.groupInfoes ? deepClone(this.data.groupInfoes) : [];
this.model = this.data && this.data.model || {};
this.model = inspector.sanitize(this.schema, this.model).data; this.model = inspector.sanitize(this.schema, this.model).data;
this.updateAndRender(); this.updateAndRender();
this.isModelValid = this.validateModel(); this.isModelValid = this.validateModel();
if (!this.isModelValid) { if (!this.isModelValid) {
this.updateView(); this.updateView();
} }
} }
updateView() { updateView() {
this.propagateChange(this.model); if (this.data) {
this.data.model = this.model;
this.propagateChange(this.data);
}
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) { for (const propName of Object.keys(changes)) {
const change = changes[propName]; const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) { if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'schema') { if (propName === 'readonly') {
this.model = inspector.sanitize(this.schema, this.model).data;
this.isModelValid = this.validateModel();
}
if (['readonly', 'schema', 'form', 'groupInfoes'].includes(propName)) {
this.updateAndRender(); this.updateAndRender();
} }
} }
} }
} }
onFullscreenChanged() {}
private onModelChange(key: (string | number)[], val: any) { private onModelChange(key: (string | number)[], val: any) {
if (isString(val) && val === '') { if (isString(val) && val === '') {
val = undefined; val = undefined;
} }
if (JsonFormUtils.updateValue(key, this.model, val)) { if (JsonFormUtils.updateValue(key, this.model, val)) {
this.formProps.model = this.model; this.formProps.model = this.model;
this.isModelValid = this.validateModel();
this.updateView(); this.updateView();
} }
} }
private onColorClick(event: MouseEvent, key: string, val: string) { private onColorClick(key: (string | number)[],
val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => {
const e = event as any; if (colorSelectedFn) {
if (e.data && e.data.onValueChanged) { colorSelectedFn(tinycolor(color).toRgb());
e.data.onValueChanged(tinycolor(color).toRgb());
} }
}); });
} }
private onToggleFullscreen() { private onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) {
this.targetFullscreenElement = element;
this.isFullscreen = !this.isFullscreen; this.isFullscreen = !this.isFullscreen;
this.formProps.isFullscreen = this.isFullscreen; this.fullscreenFinishFn = fullscreenFinishFn;
}
onFullscreenChanged(fullscreen: boolean) {
this.formProps.isFullscreen = fullscreen;
this.renderReactSchemaForm(false);
if (this.fullscreenFinishFn) {
this.fullscreenFinishFn();
this.fullscreenFinishFn = null;
}
} }
private updateAndRender() { private updateAndRender() {
const schema = this.schema ? deepClone(this.schema) : {
type: 'object'
};
schema.strict = true;
const form = this.form ? deepClone(this.form) : [ '*' ];
const groupInfoes = this.groupInfoes ? deepClone(this.groupInfoes) : [];
this.formProps.option.formDefaults.readonly = this.readonly; this.formProps.option.formDefaults.readonly = this.readonly;
this.formProps.schema = schema; this.formProps.schema = this.schema;
this.formProps.form = form; this.formProps.form = this.form;
this.formProps.groupInfoes = groupInfoes; this.formProps.groupInfoes = this.groupInfoes;
this.formProps.model = deepClone(this.model); this.formProps.model = deepClone(this.model);
this.renderReactSchemaForm(); this.renderReactSchemaForm();
} }
private renderReactSchemaForm() { private renderReactSchemaForm(destroy: boolean = true) {
if (destroy) {
this.destroyReactSchemaForm();
}
ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement); ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement);
} }

184
ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx

@ -0,0 +1,184 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import reactCSS from 'reactcss';
import ReactAce from 'react-ace';
import Button from '@material-ui/core/Button';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import * as tinycolor from 'tinycolor2';
import { IEditorProps } from 'react-ace/src/types';
interface ThingsboardAceEditorProps extends JsonFormFieldProps {
mode: string;
onTidy: (value: string) => string;
}
interface ThingsboardAceEditorState extends JsonFormFieldState {
isFull: boolean;
focused: boolean;
}
class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, ThingsboardAceEditorState> {
hostElement: HTMLElement;
private aceEditor: IEditorProps;
constructor(props) {
super(props);
this.onValueChanged = this.onValueChanged.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this);
this.onLoad = this.onLoad.bind(this);
this.onToggleFull = this.onToggleFull.bind(this);
const value = props.value ? props.value + '' : '';
this.state = {
isFull: false,
value,
focused: false
};
}
onValueChanged(value) {
this.setState({
value
});
this.props.onChangeValidate({
target: {
value
}
});
}
onBlur() {
this.setState({ focused: false });
}
onFocus() {
this.setState({ focused: true });
}
onTidy() {
if (!this.props.form.readonly) {
let value = this.state.value;
value = this.props.onTidy(value);
this.setState({
value
});
this.props.onChangeValidate({
target: {
value
}
});
}
}
onLoad(editor: IEditorProps) {
this.aceEditor = editor;
}
onToggleFull() {
this.setState({ isFull: !this.state.isFull });
this.props.onToggleFullscreen(this.hostElement, () => {
if (this.aceEditor) {
this.aceEditor.resize();
this.aceEditor.renderer.updateFull();
}
});
}
componentDidUpdate() {
}
render() {
const styles = reactCSS({
default: {
tidyButtonStyle: {
color: '#7B7B7B',
minWidth: '32px',
minHeight: '15px',
lineHeight: '15px',
fontSize: '0.800rem',
margin: '0',
padding: '4px',
height: '23px',
borderRadius: '5px',
marginLeft: '5px'
}
}
});
let labelClass = 'tb-label';
if (this.props.form.required) {
labelClass += ' tb-required';
}
if (this.props.form.readonly) {
labelClass += ' tb-readonly';
}
if (this.state.focused) {
labelClass += ' tb-focused';
}
let containerClass = 'tb-container';
const style = this.props.form.style || {width: '100%'};
if (this.state.isFull) {
containerClass += ' fullscreen-form-field';
}
return (
<div>
<div className='tb-json-form' ref={c => (this.hostElement = c)}>
<div className={containerClass}>
<label className={labelClass}>{this.props.form.title}</label>
<div className='json-form-ace-editor'>
<div className='title-panel'>
<label>{this.props.mode}</label>
<Button style={ styles.tidyButtonStyle }
className='tidy-button' onClick={this.onTidy}>Tidy</Button>
<Button style={ styles.tidyButtonStyle }
className='tidy-button' onClick={this.onToggleFull}>
{this.state.isFull ?
'Exit fullscreen' : 'Fullscreen'}
</Button>
</div>
<ReactAce mode={this.props.mode}
height={this.state.isFull ? '100%' : '150px'}
width={this.state.isFull ? '100%' : '300px'}
theme='github'
onChange={this.onValueChanged}
onFocus={this.onFocus}
onBlur={this.onBlur}
onLoad={this.onLoad}
name={this.props.form.title}
value={this.state.value}
readOnly={this.props.form.readonly}
editorProps={{$blockScrolling: Infinity}}
enableBasicAutocompletion={true}
enableSnippets={true}
enableLiveAutocompletion={true}
style={style}/>
</div>
<div className='json-form-error'
style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div>
</div>
</div>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardAceEditor);

3
ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx

@ -17,7 +17,8 @@ import * as React from 'react';
import JsonFormUtils from './json-form-utils'; import JsonFormUtils from './json-form-utils';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
export default ThingsboardBaseComponent => class extends React.Component<JsonFormFieldProps, JsonFormFieldState> { export default ThingsboardBaseComponent => class<P extends JsonFormFieldProps>
extends React.Component<P, JsonFormFieldState> {
constructor(props) { constructor(props) {
super(props); super(props);

184
ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx

@ -0,0 +1,184 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import ThingsboardBaseComponent from './json-form-base-component';
import reactCSS from 'reactcss';
import * as tinycolor from 'tinycolor2';
import TextField from '@material-ui/core/TextField';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import IconButton from '@material-ui/core/IconButton';
import ClearIcon from '@material-ui/icons/Clear';
import Tooltip from '@material-ui/core/Tooltip';
interface ThingsboardColorState extends JsonFormFieldState {
color: tinycolor.ColorFormats.RGBA | null;
focused: boolean;
}
class ThingsboardColor extends React.Component<JsonFormFieldProps, ThingsboardColorState> {
constructor(props) {
super(props);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
this.onSwatchClick = this.onSwatchClick.bind(this);
this.onClear = this.onClear.bind(this);
const value = props.value ? props.value + '' : null;
const color = value != null ? tinycolor(value).toRgb() : null;
this.state = {
color,
focused: false
};
}
onBlur() {
this.setState({focused: false});
}
onFocus() {
this.setState({focused: true});
}
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
const colContainer = $(node).children('#color-container');
colContainer.click((event) => {
if (!this.props.form.readonly) {
this.onSwatchClick(event);
}
});
}
componentWillUnmount() {
const node = ReactDOM.findDOMNode(this);
const colContainer = $(node).children('#color-container');
colContainer.off( 'click' );
}
onValueChanged(value: tinycolor.ColorFormats.RGBA | null) {
let color: tinycolor.Instance = null;
if (value != null) {
color = tinycolor(value);
}
this.setState({
color: value
});
let colorValue = '';
if (color != null && color.getAlpha() !== 1) {
colorValue = color.toRgbString();
} else if (color != null) {
colorValue = color.toHexString();
}
this.props.onChangeValidate({
target: {
value: colorValue
}
});
}
onSwatchClick(event) {
this.props.onColorClick(this.props.form.key, this.state.color,
(color) => {
this.onValueChanged(color);
}
);
}
onClear(event) {
if (event) {
event.stopPropagation();
}
this.onValueChanged(null);
}
render() {
let background = 'rgba(0,0,0,0)';
if (this.state.color != null) {
background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`;
}
const styles = reactCSS({
default: {
color: {
background: `${ background }`
},
swatch: {
display: 'inline-block',
marginRight: '10px',
marginTop: 'auto',
marginBottom: 'auto',
cursor: 'pointer',
opacity: `${ this.props.form.readonly ? '0.6' : '1' }`
},
swatchText: {
width: '100%'
},
container: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
colorContainer: {
display: 'flex',
width: '100%'
}
},
});
let fieldClass = 'tb-field';
if (this.props.form.required) {
fieldClass += ' tb-required';
}
if (this.props.form.readonly) {
fieldClass += ' tb-readonly';
}
if (this.state.focused) {
fieldClass += ' tb-focused';
}
let stringColor = '';
if (this.state.color != null) {
const color = tinycolor(this.state.color);
stringColor = color.toRgbString();
}
return (
<div style={ styles.container }>
<div id='color-container' style={ styles.colorContainer }>
<div className='tb-color-preview' style={ styles.swatch }>
<div className='tb-color-result' style={ styles.color }/>
</div>
<TextField
className={fieldClass}
label={this.props.form.title}
error={!this.props.valid}
helperText={this.props.valid ? this.props.form.placeholder : this.props.error}
value={stringColor}
disabled={this.props.form.readonly}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={ styles.swatchText }/>
</div>
<Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><ClearIcon/></IconButton></Tooltip>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardColor);

39
ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx

@ -0,0 +1,39 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { css_beautify } from 'js-beautify';
class ThingsboardCss extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props) {
super(props);
this.onTidyCss = this.onTidyCss.bind(this);
}
onTidyCss(css: string): string {
return css_beautify(css, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='css' onTidy={this.onTidyCss} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardCss;

39
ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx

@ -0,0 +1,39 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { html_beautify } from 'js-beautify';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
class ThingsboardHtml extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props) {
super(props);
this.onTidyHtml = this.onTidyHtml.bind(this);
}
onTidyHtml(html: string): string {
return html_beautify(html, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='html' onTidy={this.onTidyHtml} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardHtml;

109
ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx

@ -0,0 +1,109 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {useCallback} from 'react';
import { DropzoneState, useDropzone } from 'react-dropzone';
import ThingsboardBaseComponent from './json-form-base-component';
import Dropzone from 'react-dropzone';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import IconButton from '@material-ui/core/IconButton';
import ClearIcon from '@material-ui/icons/Clear';
import Tooltip from '@material-ui/core/Tooltip';
interface ThingsboardImageState extends JsonFormFieldState {
imageUrl: string;
}
class ThingsboardImage extends React.Component<JsonFormFieldProps, ThingsboardImageState> {
constructor(props) {
super(props);
this.onDrop = this.onDrop.bind(this);
this.onClear = this.onClear.bind(this);
const value = props.value ? props.value + '' : null;
this.state = {
imageUrl: value
};
}
onDrop(acceptedFiles: File[]) {
const reader = new FileReader();
reader.onload = () => {
this.onValueChanged(reader.result);
};
reader.readAsDataURL(acceptedFiles[0]);
}
onValueChanged(value) {
this.setState({
imageUrl: value
});
this.props.onChangeValidate({
target: {
value
}
});
}
onClear(event) {
if (event) {
event.stopPropagation();
}
this.onValueChanged('');
}
render() {
let labelClass = 'tb-label';
if (this.props.form.required) {
labelClass += ' tb-required';
}
if (this.props.form.readonly) {
labelClass += ' tb-readonly';
}
let previewComponent;
if (this.state.imageUrl) {
previewComponent = <img className='tb-image-preview' src={this.state.imageUrl} />;
} else {
previewComponent = <div>No image selected</div>;
}
return (
<div className='tb-container'>
<label className={labelClass}>{this.props.form.title}</label>
<div className='tb-image-select-container'>
<div className='tb-image-preview-container'>{previewComponent}</div>
<div className='tb-image-clear-container'>
<Tooltip title='Clear' placement='top'>
<IconButton className='tb-image-clear-btn' onClick={this.onClear}><ClearIcon/></IconButton>
</Tooltip>
</div>
<Dropzone onDrop={this.onDrop}
accept='image/*' multiple={false}>
{({getRootProps, getInputProps}) => (
<div className='tb-dropzone' {...getRootProps()}>
<div>Drop an image or click to select a file to upload.</div>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
</div>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardImage);

38
ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx

@ -0,0 +1,38 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
class ThingsboardJavaScript extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props) {
super(props);
this.onTidyJavascript = this.onTidyJavascript.bind(this);
}
onTidyJavascript(javascript: string): string {
return js_beautify(javascript, {indent_size: 4, wrap_line_length: 60});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='javascript' onTidy={this.onTidyJavascript} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardJavaScript;

38
ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx

@ -0,0 +1,38 @@
/*
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
class ThingsboardJson extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props) {
super(props);
this.onTidyJson = this.onTidyJson.bind(this);
}
onTidyJson(json: string): string {
return js_beautify(json, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='json' onTidy={this.onTidyJson} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardJson;

44
ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx

@ -17,24 +17,25 @@ import * as React from 'react';
import JsonFormUtils from './json-form-utils'; import JsonFormUtils from './json-form-utils';
import ThingsboardArray from './json-form-array'; import ThingsboardArray from './json-form-array';
/*import ThingsboardJavaScript from './json-form-javascript.jsx'; import ThingsboardJavaScript from './json-form-javascript';
import ThingsboardJson from './json-form-json.jsx'; import ThingsboardJson from './json-form-json';
import ThingsboardHtml from './json-form-html.jsx'; import ThingsboardHtml from './json-form-html';
import ThingsboardCss from './json-form-css.jsx'; import ThingsboardCss from './json-form-css';
import ThingsboardColor from './json-form-color.jsx'*/ import ThingsboardColor from './json-form-color';
import ThingsboardRcSelect from './json-form-rc-select'; import ThingsboardRcSelect from './json-form-rc-select';
import ThingsboardNumber from './json-form-number'; import ThingsboardNumber from './json-form-number';
import ThingsboardText from './json-form-text'; import ThingsboardText from './json-form-text';
import ThingsboardSelect from './json-form-select'; import ThingsboardSelect from './json-form-select';
import ThingsboardRadios from './json-form-radios'; import ThingsboardRadios from './json-form-radios';
import ThingsboardDate from './json-form-date'; import ThingsboardDate from './json-form-date';
/*import ThingsboardImage from './json-form-image.jsx';*/ import ThingsboardImage from './json-form-image';
import ThingsboardCheckbox from './json-form-checkbox'; import ThingsboardCheckbox from './json-form-checkbox';
import ThingsboardHelp from './json-form-help'; import ThingsboardHelp from './json-form-help';
import ThingsboardFieldSet from './json-form-fieldset'; import ThingsboardFieldSet from './json-form-fieldset';
import { JsonFormProps, GroupInfo, JsonFormData } from './json-form.models'; import { JsonFormProps, GroupInfo, JsonFormData, onChangeFn, OnColorClickFn } from './json-form.models';
import _ from 'lodash'; import _ from 'lodash';
import * as tinycolor from 'tinycolor2';
class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
select: ThingsboardSelect, select: ThingsboardSelect,
radios: ThingsboardRadios, radios: ThingsboardRadios,
date: ThingsboardDate, date: ThingsboardDate,
// image: ThingsboardImage, image: ThingsboardImage,
checkbox: ThingsboardCheckbox, checkbox: ThingsboardCheckbox,
help: ThingsboardHelp, help: ThingsboardHelp,
array: ThingsboardArray, array: ThingsboardArray,
// javascript: ThingsboardJavaScript, javascript: ThingsboardJavaScript,
// json: ThingsboardJson, json: ThingsboardJson,
// html: ThingsboardHtml, html: ThingsboardHtml,
// css: ThingsboardCss, css: ThingsboardCss,
// color: ThingsboardColor, color: ThingsboardColor,
'rc-select': ThingsboardRcSelect, 'rc-select': ThingsboardRcSelect,
fieldset: ThingsboardFieldSet fieldset: ThingsboardFieldSet
}; };
@ -78,20 +79,21 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
} }
} }
onColorClick(event, key, val) { onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
this.props.onColorClick(event, key, val); colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.props.onColorClick(key, val, colorSelectedFn);
} }
onToggleFullscreen() { onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) {
this.props.onToggleFullscreen(); this.props.onToggleFullscreen(element, fullscreenFinishFn);
} }
builder(form: JsonFormData, builder(form: JsonFormData,
model: any, model: any,
index: number, index: number,
onChange: (key: (string | number)[], val: any) => void, onChange: onChangeFn,
onColorClick: (event: MouseEvent, key: string, val: string) => void, onColorClick: OnColorClickFn,
onToggleFullscreen: () => void, onToggleFullscreen: () => void,
mapper: {[type: string]: any}): JSX.Element { mapper: {[type: string]: any}): JSX.Element {
const type = form.type; const type = form.type;
@ -173,9 +175,9 @@ class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps
} }
render() { render() {
const theCla = 'pull-right fa fa-chevron-down md-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); const theCla = 'pull-right fa fa-chevron-down tb-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled');
return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}> return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}>
<div className='SchemaGroupname md-button-toggle' <div className='SchemaGroupname tb-button-toggle'
onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div> onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
<div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div> <div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div>
</section>); </section>);

12
ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts

@ -544,11 +544,13 @@ function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[])
const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => { const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => {
$fn($schema, $path); $fn($schema, $path);
for (const k of Object.keys($schema.properties)) { if ($schema.properties) {
if ($schema.properties.hasOwnProperty(k)) { for (const k of Object.keys($schema.properties)) {
const currentPath = $path.slice(); if ($schema.properties.hasOwnProperty(k)) {
currentPath.push(k); const currentPath = $path.slice();
traverse($schema.properties[k], $fn, currentPath); currentPath.push(k);
traverse($schema.properties[k], $fn, currentPath);
}
} }
} }
if (!ignoreArrays && $schema.items) { if (!ignoreArrays && $schema.items) {

36
ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts

@ -18,12 +18,13 @@ import { isUndefined, isDefined, isString } from '@app/core/utils';
import * as equal from 'deep-equal'; import * as equal from 'deep-equal';
import ObjectPath from 'objectpath'; import ObjectPath from 'objectpath';
import * as React from 'react'; import * as React from 'react';
import * as tinycolor from 'tinycolor2';
export interface SchemaValidationResult { export interface SchemaValidationResult {
valid: boolean; valid: boolean;
error?: { error?: {
message?: string; message?: string;
} };
} }
export interface FormOption { export interface FormOption {
@ -47,6 +48,11 @@ export interface GroupInfo {
GroupTitle: string; GroupTitle: string;
} }
export type onChangeFn = (key: (string | number)[], val: any) => void;
export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void;
export type onToggleFullscreenFn = (element: HTMLElement, fullscreenFinishFn?: () => void) => void;
export interface JsonFormProps { export interface JsonFormProps {
model?: any; model?: any;
schema?: any; schema?: any;
@ -55,9 +61,9 @@ export interface JsonFormProps {
isFullscreen: boolean; isFullscreen: boolean;
ignore?: {[key: string]: boolean}; ignore?: {[key: string]: boolean};
option: FormOption; option: FormOption;
onModelChange?: (key: (string | number)[], val: any) => void; onModelChange?: onChangeFn;
onColorClick?: (event: MouseEvent, key: string, val: string) => void; onColorClick?: OnColorClickFn;
onToggleFullscreen?: () => void; onToggleFullscreen?: onToggleFullscreenFn;
mapper?: {[type: string]: any}; mapper?: {[type: string]: any};
} }
@ -98,22 +104,24 @@ export interface JsonFormData {
[key: string]: any; [key: string]: any;
} }
export type ComponentBuilderFn = (form: JsonFormData,
model: any,
index: number,
onChange: onChangeFn,
onColorClick: OnColorClickFn,
onToggleFullscreen: onToggleFullscreenFn,
mapper: {[type: string]: any}) => JSX.Element;
export interface JsonFormFieldProps { export interface JsonFormFieldProps {
value: any; value: any;
model: any; model: any;
form: JsonFormData; form: JsonFormData;
builder: (form: JsonFormData, builder: ComponentBuilderFn;
model: any,
index: number,
onChange: (key: (string | number)[], val: any) => void,
onColorClick: (event: MouseEvent, key: string, val: string) => void,
onToggleFullscreen: () => void,
mapper: {[type: string]: any}) => JSX.Element;
mapper?: {[type: string]: any}; mapper?: {[type: string]: any};
onChange?: (key: (string | number)[], val: any) => void; onChange?: onChangeFn;
onColorClick?: (event: MouseEvent, key: string, val: string) => void; onColorClick?: OnColorClickFn;
onChangeValidate?: (e: any) => void; onChangeValidate?: (e: any) => void;
onToggleFullscreen?: () => void; onToggleFullscreen?: onToggleFullscreenFn;
valid?: boolean; valid?: boolean;
error?: string; error?: string;
options?: { options?: {

177
ui-ngx/src/app/shared/components/json-form/react/json-form.scss

@ -21,47 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
$input-label-float-offset: 6px !default; $input-label-float-offset: 6px !default;
$input-label-float-scale: .75 !default; $input-label-float-scale: .75 !default;
.tb-json-form { $previewSize: 100px !default;
&.tb-fullscreen {
[name="ReactSchemaForm"] {
.SchemaForm {
&.SchemaFormFullscreen {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
> div:not(.fullscreen-form-field) {
display: none !important;
}
> div.fullscreen-form-field {
position: relative;
width: 100%;
height: 100%;
}
}
}
> div { .tb-json-form {
> section {
margin: 0 !important;
box-shadow: none !important;
.SchemaGroupname {
display: none !important;
}
> div { &.tb-fullscreen {
padding: 0 !important; background: #fff;
} position: absolute;
} top: 0;
} right: 0;
bottom: 0;
left: 0;
> div.fullscreen-form-field {
position: relative;
width: 100%;
height: 100%;
} }
} }
.json-form-error { .json-form-error {
position: relative; position: relative;
bottom: -5px; bottom: -5px;
@ -74,6 +51,7 @@ $input-label-float-scale: .75 !default;
.tb-container { .tb-container {
position: relative; position: relative;
box-sizing: border-box;
padding: 10px 0; padding: 10px 0;
margin-top: 32px; margin-top: 32px;
} }
@ -144,6 +122,7 @@ $input-label-float-scale: .75 !default;
.tb-head-label { .tb-head-label {
color: rgba(0, 0, 0, .54); color: rgba(0, 0, 0, .54);
padding-bottom: 15px;
} }
.SchemaGroupname { .SchemaGroupname {
@ -154,4 +133,130 @@ $input-label-float-scale: .75 !default;
.invisible { .invisible {
display: none; display: none;
} }
span.tb-toggle-icon {
padding-top: 12px;
padding-bottom: 12px;
}
.tb-button-toggle .tb-toggle-icon {
display: inline-block;
width: 15px;
margin: auto 0 auto auto;
background-size: 100% auto;
transition: transform .3s, ease-in-out;
}
.tb-button-toggle .tb-toggle-icon.tb-toggled {
transform: rotateZ(180deg);
}
.fullscreen-form-field {
.json-form-ace-editor {
height: calc(100% - 60px);
}
}
.json-form-ace-editor {
position: relative;
height: 100%;
border: 1px solid #c0c0c0;
.title-panel {
position: absolute;
top: 10px;
right: 20px;
z-index: 5;
font-size: .8rem;
font-weight: 500;
label {
padding: 4px;
color: #00acc1;
text-transform: uppercase;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
}
button.tidy-button {
background: rgba(220, 220, 220, .35) !important;
span {
padding: 0 !important;
font-size: 12px !important;
}
}
}
}
.tb-image-select-container {
position: relative;
width: 100%;
height: $previewSize;
}
.tb-image-preview {
width: auto;
max-width: $previewSize;
height: auto;
max-height: $previewSize;
}
.tb-image-preview-container {
position: relative;
float: left;
width: $previewSize;
height: $previewSize;
margin-right: 12px;
vertical-align: top;
border: solid 1px;
div {
width: 100%;
font-size: 18px;
text-align: center;
}
div, .tb-image-preview {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.tb-dropzone {
outline: none;
position: relative;
height: $previewSize;
padding: 0 8px;
overflow: hidden;
vertical-align: top;
border: dashed 2px;
div {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
font-size: 24px;
text-align: center;
transform: translate(-50%, -50%);
}
}
.tb-image-clear-container {
position: relative;
float: right;
width: 48px;
height: $previewSize;
}
.tb-image-clear-btn {
position: absolute !important;
top: 50%;
transform: translate(0%, -50%) !important;
}
} }

7
ui-ngx/src/app/shared/models/widget.models.ts

@ -283,11 +283,6 @@ export interface LegendData {
data: Array<LegendKeyData>; data: Array<LegendKeyData>;
} }
export interface WidgetConfigSettings {
[key: string]: any;
// TODO:
}
export enum WidgetActionType { export enum WidgetActionType {
openDashboardState = 'openDashboardState', openDashboardState = 'openDashboardState',
updateDashboardState = 'updateDashboardState', updateDashboardState = 'updateDashboardState',
@ -347,7 +342,7 @@ export interface WidgetConfig {
units?: string; units?: string;
decimals?: number; decimals?: number;
actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
settings?: WidgetConfigSettings; settings?: any;
alarmSource?: Datasource; alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus; alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number; alarmsPollingInterval?: number;

9
ui-ngx/src/scss/mixins.scss

@ -39,3 +39,12 @@
margin: auto; margin: auto;
} }
} }
@mixin tb-checkered-bg() {
background-color: #fff;
background-image:
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd),
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd);
background-position: 0 0, 4px 4px;
background-size: 8px 8px;
}

21
ui-ngx/src/styles.scss

@ -852,4 +852,25 @@ mat-label {
} }
} }
} }
.tb-color-preview {
cursor: pointer;
box-sizing: border-box;
position: relative;
width: 24px;
min-width: 24px;
height: 24px;
overflow: hidden;
content: "";
border: 2px solid #fff;
border-radius: 50%;
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);
@include tb-checkered-bg();
.tb-color-result {
width: 100%;
height: 100%;
}
}
} }

Loading…
Cancel
Save