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

2
ui-ngx/package.json

@ -41,7 +41,7 @@
"base64-js": "^1.3.1",
"compass-sass-mixins": "^0.12.7",
"core-js": "^3.1.4",
"date-fns": "^2.5.0",
"date-fns": "2.1.0",
"deep-equal": "^1.0.1",
"flot": "git://github.com/thingsboard/flot.git#0.9-work",
"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
formControlName="entityAlias"
(focusin)="onFocus()"
[required]="required"
[required]="tbRequired"
(keydown)="entityAliasEnter($event)"
(keypress)="entityAliasEnter($event)"
[matAutocomplete]="entityAliasAutocomplete">
@ -54,7 +54,7 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!modelValue && required">
<mat-error *ngIf="!modelValue && tbRequired">
{{ 'entity.alias-required' | translate }}
</mat-error>
</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;
get required(): boolean {
get tbRequired(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
set tbRequired(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@ -151,7 +151,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.required && !this.modelValue;
const customErrorState = this.tbRequired && !this.modelValue;
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}"
[formGroup]="dataKeyFormGroup"
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">
<mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function">
<mat-label>{{ 'entity.key' | translate }}</mat-label>
@ -94,12 +93,10 @@
</section>
</div>
</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 style="overflow: auto;">
<tb-json-form
[schema]="dataKeySchema"
[form]="dataKeyForm"
formControlName="settings">
</tb-json-form>
</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 { alarmFields } from '@shared/models/alarm.models';
import { JsFuncComponent } from '@shared/components/js-func.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
@Component({
selector: 'tb-data-key-config',
@ -77,15 +78,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
displayAdvanced = false;
dataKeySchema: any;
dataKeyForm: any;
private modelValue: DataKey;
private propagateChange = null;
public dataKeyFormGroup: FormGroup;
public dataKeySettingsFormGroup: FormGroup;
private dataKeySettingsData: JsonFormComponentData;
private alarmKeys: Array<DataKey>;
filteredKeys: Observable<Array<string>>;
@ -112,8 +114,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) {
this.displayAdvanced = true;
this.dataKeySchema = this.dataKeySettingsSchema.schema;
this.dataKeyForm = this.dataKeySettingsSchema.form || ['*'];
this.dataKeySettingsData = {
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({
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*$/)]],
funcBody: [null, []],
usePostProcessing: [null, []],
postFuncBody: [null, []],
settings: [null, []]
postFuncBody: [null, []]
});
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.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []);
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() {
this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value};
if (this.displayAdvanced) {
this.modelValue.settings = this.dataKeySettingsFormGroup.get('settings').value.model;
}
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;
}
}

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

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

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

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

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

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

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

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

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

@ -71,6 +71,7 @@ import {
EntityAliasesDialogData
} from '@home/components/alias/entity-aliases-dialog.component';
import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component';
@Component({
selector: 'tb-dashboard-page',
@ -109,7 +110,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
editingWidgetLayoutOriginal: WidgetLayout = null;
editingWidgetSubtitle: string = null;
editingLayoutCtx: DashboardPageLayoutContext = null;
editingWidgetFormGroup: FormGroup;
thingsboardVersion: string = env.tbVersion;
@ -188,6 +188,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
set rightLayoutOpened(rightLayoutOpened: boolean) {
}
@ViewChild('tbEditWidget', {static: false}) editWidgetComponent: EditWidgetComponent;
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
private breakpointObserver: BreakpointObserver,
@ -205,10 +207,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private dialog: MatDialog) {
super(store);
this.editingWidgetFormGroup = this.fb.group({
widgetConfig: [null]
});
this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
this.init(data);
@ -630,15 +628,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
onRevertWidgetEdit() {
if (this.editingWidgetFormGroup.dirty) {
this.editingWidgetFormGroup.markAsPristine();
if (this.editWidgetComponent.widgetFormGroup.dirty) {
this.editWidgetComponent.widgetFormGroup.markAsPristine();
this.editingWidget = deepClone(this.editingWidgetOriginal);
this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
}
}
saveWidget() {
this.editingWidgetFormGroup.markAsPristine();
this.editWidgetComponent.widgetFormGroup.markAsPristine();
const widget = deepClone(this.editingWidget);
const widgetLayout = deepClone(this.editingWidgetLayout);
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.
-->
<form [formGroup]="widgetFormGroup">
<form #widgetForm="ngForm" [formGroup]="widgetFormGroup">
<fieldset [disabled]="isLoading$ | async">
<tb-widget-config
[typeParameters]="typeParameters"
[actionSources]="actionSources"
[isDataEnabled]="isDataEnabled"
[widgetSettingsSchema]="settingsSchema"
[dataKeySettingsSchema]="dataKeySettingsSchema"
[aliasController]="aliasController"
[functionsOnly]="widgetEditMode"
[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.
///
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 { Store } from '@ngrx/store';
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 { WidgetConfigComponentData } from '../../models/widget-component.models';
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 { Observable, of } from 'rxjs';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
@ -66,21 +66,20 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input()
widgetLayout: WidgetLayout;
@Input()
@ViewChild('widgetForm', {static: true}) widgetForm: NgForm;
widgetFormGroup: FormGroup;
widgetConfig: WidgetConfigComponentData;
typeParameters: WidgetTypeParameters;
actionSources: {[key: string]: WidgetActionSource};
isDataEnabled: boolean;
settingsSchema: any;
dataKeySettingsSchema: any;
functionsOnly: boolean;
constructor(protected store: Store<AppState>,
private dialog: MatDialog,
private fb: FormBuilder,
private widgetComponentService: WidgetComponentService) {
super(store);
this.widgetFormGroup = this.fb.group({
widgetConfig: [null]
});
}
ngOnInit(): void {
@ -104,27 +103,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
private loadWidgetConfig() {
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
this.widgetConfig = {
config: this.widget.config,
layout: this.widgetLayout,
widgetType: this.widget.type
};
const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
this.typeParameters = widgetInfo.typeParameters;
this.actionSources = widgetInfo.actionSources;
this.isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
if (!settingsSchema || settingsSchema === '') {
this.settingsSchema = {};
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const typeParameters = widgetInfo.typeParameters;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};
} else {
this.settingsSchema = isString(settingsSchema) ? JSON.parse(settingsSchema) : settingsSchema;
settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema;
}
if (!dataKeySettingsSchema || dataKeySettingsSchema === '') {
this.dataKeySettingsSchema = {};
let dataKeySettingsSchema;
if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') {
dataKeySettingsSchema = {};
} 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});
}

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
* 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 {
.mat-form-field {
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,
ElementRef,
EventEmitter,
Input,
Output,
Input, OnChanges,
Output, SimpleChanges,
ViewContainerRef
} from '@angular/core';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
@Directive({
selector: '[tb-fullscreen]'
})
export class FullscreenDirective {
export class FullscreenDirective implements OnChanges {
fullscreenValue = false;
@ -37,16 +37,10 @@ export class FullscreenDirective {
private parentElement: HTMLElement;
@Input()
set fullscreen(fullscreen: boolean) {
if (this.fullscreenValue !== fullscreen) {
this.fullscreenValue = fullscreen;
if (this.fullscreenValue) {
this.enterFullscreen();
} else {
this.exitFullscreen();
}
}
}
fullscreen: boolean;
@Input()
fullscreenElement: HTMLElement;
@Output()
fullscreenChanged = new EventEmitter<boolean>();
@ -56,11 +50,30 @@ export class FullscreenDirective {
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() {
const targetElement = this.elementRef;
this.parentElement = targetElement.nativeElement.parentElement;
this.parentElement.removeChild(targetElement.nativeElement);
targetElement.nativeElement.classList.add('tb-fullscreen');
const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
this.parentElement = targetElement.parentElement;
this.parentElement.removeChild(targetElement);
targetElement.classList.add('tb-fullscreen');
const position = this.overlay.position();
const config = new OverlayConfig({
hasBackdrop: false,
@ -73,19 +86,19 @@ export class FullscreenDirective {
this.overlayRef = this.overlay.create(config);
this.overlayRef.attach(new EmptyPortal());
this.overlayRef.overlayElement.append( targetElement.nativeElement );
this.overlayRef.overlayElement.append( targetElement );
this.fullscreenChanged.emit(true);
}
exitFullscreen() {
const targetElement = this.elementRef;
const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement;
if (this.parentElement) {
this.overlayRef.overlayElement.removeChild( targetElement.nativeElement );
this.parentElement.append(targetElement.nativeElement);
this.overlayRef.overlayElement.removeChild( targetElement );
this.parentElement.append(targetElement);
this.parentElement = null;
}
targetElement.nativeElement.classList.remove('tb-fullscreen');
if (this.elementRef !== targetElement) {
targetElement.classList.remove('tb-fullscreen');
if (this.elementRef) {
this.elementRef.nativeElement.classList.remove('tb-fullscreen');
}
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.
-->
<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreen]="isFullscreen"
(fullscreenChanged)="onFullscreenChanged()">
<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreenElement]="targetFullscreenElement"
[fullscreen]="isFullscreen"
(fullscreenChanged)="onFullscreenChanged($event)">
<div #reactRoot></div>
<div>{{ model | json }}</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 ReactSchemaForm from './react/json-form-react';
import JsonFormUtils from './react/json-form-utils';
import { JsonFormComponentData } from './json-form-component.models';
@Component({
selector: 'tb-json-form',
@ -64,12 +65,6 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
@ViewChild('reactRoot', {static: true})
reactRootElmRef: ElementRef<HTMLElement>;
@Input() schema: any;
@Input() form: any;
@Input() groupInfoes: any[];
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
@ -91,11 +86,18 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
onToggleFullscreen: this.onToggleFullscreen.bind(this)
};
data: JsonFormComponentData;
model: any;
schema: any;
form: any;
groupInfoes: any[];
isModelValid = true;
isFullscreen = false;
targetFullscreenElement: HTMLElement;
fullscreenFinishFn: () => void;
private propagateChange = null;
@ -128,77 +130,91 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
};
}
writeValue(value: any): void {
this.model = value || {};
writeValue(data: JsonFormComponentData): void {
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.updateAndRender();
this.isModelValid = this.validateModel();
if (!this.isModelValid) {
this.updateView();
}
}
}
updateView() {
this.propagateChange(this.model);
if (this.data) {
this.data.model = this.model;
this.propagateChange(this.data);
}
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'schema') {
this.model = inspector.sanitize(this.schema, this.model).data;
this.isModelValid = this.validateModel();
}
if (['readonly', 'schema', 'form', 'groupInfoes'].includes(propName)) {
if (propName === 'readonly') {
this.updateAndRender();
}
}
}
}
onFullscreenChanged() {}
private onModelChange(key: (string | number)[], val: any) {
if (isString(val) && val === '') {
val = undefined;
}
if (JsonFormUtils.updateValue(key, this.model, val)) {
this.formProps.model = this.model;
this.isModelValid = this.validateModel();
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) => {
const e = event as any;
if (e.data && e.data.onValueChanged) {
e.data.onValueChanged(tinycolor(color).toRgb());
if (colorSelectedFn) {
colorSelectedFn(tinycolor(color).toRgb());
}
});
}
private onToggleFullscreen() {
private onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) {
this.targetFullscreenElement = element;
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() {
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.schema = schema;
this.formProps.form = form;
this.formProps.groupInfoes = groupInfoes;
this.formProps.schema = this.schema;
this.formProps.form = this.form;
this.formProps.groupInfoes = this.groupInfoes;
this.formProps.model = deepClone(this.model);
this.renderReactSchemaForm();
}
private renderReactSchemaForm() {
private renderReactSchemaForm(destroy: boolean = true) {
if (destroy) {
this.destroyReactSchemaForm();
}
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 { 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) {
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 ThingsboardArray from './json-form-array';
/*import ThingsboardJavaScript from './json-form-javascript.jsx';
import ThingsboardJson from './json-form-json.jsx';
import ThingsboardHtml from './json-form-html.jsx';
import ThingsboardCss from './json-form-css.jsx';
import ThingsboardColor from './json-form-color.jsx'*/
import ThingsboardJavaScript from './json-form-javascript';
import ThingsboardJson from './json-form-json';
import ThingsboardHtml from './json-form-html';
import ThingsboardCss from './json-form-css';
import ThingsboardColor from './json-form-color';
import ThingsboardRcSelect from './json-form-rc-select';
import ThingsboardNumber from './json-form-number';
import ThingsboardText from './json-form-text';
import ThingsboardSelect from './json-form-select';
import ThingsboardRadios from './json-form-radios';
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 ThingsboardHelp from './json-form-help';
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 * as tinycolor from 'tinycolor2';
class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
select: ThingsboardSelect,
radios: ThingsboardRadios,
date: ThingsboardDate,
// image: ThingsboardImage,
image: ThingsboardImage,
checkbox: ThingsboardCheckbox,
help: ThingsboardHelp,
array: ThingsboardArray,
// javascript: ThingsboardJavaScript,
// json: ThingsboardJson,
// html: ThingsboardHtml,
// css: ThingsboardCss,
// color: ThingsboardColor,
javascript: ThingsboardJavaScript,
json: ThingsboardJson,
html: ThingsboardHtml,
css: ThingsboardCss,
color: ThingsboardColor,
'rc-select': ThingsboardRcSelect,
fieldset: ThingsboardFieldSet
};
@ -78,20 +79,21 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
}
}
onColorClick(event, key, val) {
this.props.onColorClick(event, key, val);
onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.props.onColorClick(key, val, colorSelectedFn);
}
onToggleFullscreen() {
this.props.onToggleFullscreen();
onToggleFullscreen(element: HTMLElement, fullscreenFinishFn?: () => void) {
this.props.onToggleFullscreen(element, fullscreenFinishFn);
}
builder(form: JsonFormData,
model: any,
index: number,
onChange: (key: (string | number)[], val: any) => void,
onColorClick: (event: MouseEvent, key: string, val: string) => void,
onChange: onChangeFn,
onColorClick: OnColorClickFn,
onToggleFullscreen: () => void,
mapper: {[type: string]: any}): JSX.Element {
const type = form.type;
@ -173,9 +175,9 @@ class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps
}
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'}}>
<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>
<div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div>
</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[]) => {
$fn($schema, $path);
for (const k of Object.keys($schema.properties)) {
if ($schema.properties.hasOwnProperty(k)) {
const currentPath = $path.slice();
currentPath.push(k);
traverse($schema.properties[k], $fn, currentPath);
if ($schema.properties) {
for (const k of Object.keys($schema.properties)) {
if ($schema.properties.hasOwnProperty(k)) {
const currentPath = $path.slice();
currentPath.push(k);
traverse($schema.properties[k], $fn, currentPath);
}
}
}
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 ObjectPath from 'objectpath';
import * as React from 'react';
import * as tinycolor from 'tinycolor2';
export interface SchemaValidationResult {
valid: boolean;
error?: {
message?: string;
}
};
}
export interface FormOption {
@ -47,6 +48,11 @@ export interface GroupInfo {
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 {
model?: any;
schema?: any;
@ -55,9 +61,9 @@ export interface JsonFormProps {
isFullscreen: boolean;
ignore?: {[key: string]: boolean};
option: FormOption;
onModelChange?: (key: (string | number)[], val: any) => void;
onColorClick?: (event: MouseEvent, key: string, val: string) => void;
onToggleFullscreen?: () => void;
onModelChange?: onChangeFn;
onColorClick?: OnColorClickFn;
onToggleFullscreen?: onToggleFullscreenFn;
mapper?: {[type: string]: any};
}
@ -98,22 +104,24 @@ export interface JsonFormData {
[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 {
value: any;
model: any;
form: JsonFormData;
builder: (form: JsonFormData,
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;
builder: ComponentBuilderFn;
mapper?: {[type: string]: any};
onChange?: (key: (string | number)[], val: any) => void;
onColorClick?: (event: MouseEvent, key: string, val: string) => void;
onChange?: onChangeFn;
onColorClick?: OnColorClickFn;
onChangeValidate?: (e: any) => void;
onToggleFullscreen?: () => void;
onToggleFullscreen?: onToggleFullscreenFn;
valid?: boolean;
error?: string;
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-scale: .75 !default;
.tb-json-form {
&.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%;
}
}
}
$previewSize: 100px !default;
> div {
> section {
margin: 0 !important;
box-shadow: none !important;
.SchemaGroupname {
display: none !important;
}
.tb-json-form {
> div {
padding: 0 !important;
}
}
}
&.tb-fullscreen {
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 {
position: relative;
bottom: -5px;
@ -74,6 +51,7 @@ $input-label-float-scale: .75 !default;
.tb-container {
position: relative;
box-sizing: border-box;
padding: 10px 0;
margin-top: 32px;
}
@ -144,6 +122,7 @@ $input-label-float-scale: .75 !default;
.tb-head-label {
color: rgba(0, 0, 0, .54);
padding-bottom: 15px;
}
.SchemaGroupname {
@ -154,4 +133,130 @@ $input-label-float-scale: .75 !default;
.invisible {
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>;
}
export interface WidgetConfigSettings {
[key: string]: any;
// TODO:
}
export enum WidgetActionType {
openDashboardState = 'openDashboardState',
updateDashboardState = 'updateDashboardState',
@ -347,7 +342,7 @@ export interface WidgetConfig {
units?: string;
decimals?: number;
actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
settings?: WidgetConfigSettings;
settings?: any;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;

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

@ -39,3 +39,12 @@
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