From c317be2a65773932663038c5a770edfb0531d991 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 21 Oct 2019 19:57:18 +0300 Subject: [PATCH] Implement React Schema Form. --- ui-ngx/package-lock.json | 12 +- ui-ngx/package.json | 2 +- .../alias/entity-alias-select.component.html | 4 +- .../alias/entity-alias-select.component.ts | 6 +- .../widget/data-key-config.component.html | 7 +- .../widget/data-key-config.component.ts | 39 +++- .../widget/data-keys.component.scss | 30 --- .../widget/widget-config.component.html | 46 +++-- .../widget/widget-config.component.ts | 120 ++++++------ .../home/models/widget-component.models.ts | 8 +- .../dashboard/dashboard-page.component.html | 21 +- .../dashboard/dashboard-page.component.ts | 14 +- .../dashboard/edit-widget.component.html | 7 +- .../pages/dashboard/edit-widget.component.ts | 57 +++--- .../components/color-input.component.scss | 28 --- .../shared/components/fullscreen.directive.ts | 59 +++--- .../json-form/json-form-component.models.ts | 23 +++ .../json-form/json-form.component.html | 6 +- .../json-form/json-form.component.ts | 82 ++++---- .../json-form/react/json-form-ace-editor.tsx | 184 ++++++++++++++++++ .../react/json-form-base-component.tsx | 3 +- .../json-form/react/json-form-color.tsx | 184 ++++++++++++++++++ .../json-form/react/json-form-css.tsx | 39 ++++ .../json-form/react/json-form-html.tsx | 39 ++++ .../json-form/react/json-form-image.tsx | 109 +++++++++++ .../json-form/react/json-form-javascript.tsx | 38 ++++ .../json-form/react/json-form-json.tsx | 38 ++++ .../json-form/react/json-form-schema-form.tsx | 44 +++-- .../json-form/react/json-form-utils.ts | 12 +- .../json-form/react/json-form.models.ts | 36 ++-- .../components/json-form/react/json-form.scss | 177 +++++++++++++---- ui-ngx/src/app/shared/models/widget.models.ts | 7 +- ui-ngx/src/scss/mixins.scss | 9 + ui-ngx/src/styles.scss | 21 ++ 34 files changed, 1151 insertions(+), 360 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx create mode 100644 ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx diff --git a/ui-ngx/package-lock.json b/ui-ngx/package-lock.json index 705b838aef..4e56832b72 100644 --- a/ui-ngx/package-lock.json +++ b/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": { diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 53a9138cad..4a4b2e5ee1 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html index d077352983..c88483e9cc 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html +++ b/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 @@ - + {{ 'entity.alias-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts index 16f140e50f..d7759fac21 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts +++ b/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; } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index 4b9f7e859a..890e040cc8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -16,9 +16,8 @@ --> - +
{{ 'entity.key' | translate }} @@ -94,12 +93,10 @@
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index 0ce3f958e9..f9262ffb08 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/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; filteredKeys: Observable>; @@ -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; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss index 3ec2000e69..409a4444d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss +++ b/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%; - } - } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 302072d1ae..837d86f7fd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -31,7 +31,7 @@
- widget-config.timewindow + widget-config.timewindow @@ -64,12 +64,12 @@ + modelValue?.isDataEnabled" [expanded]="true">
widget-config.datasources
-
{{ 'widget-config.maximum-datasources' | translate:{count: typeParameters.maxDatasources} }}
+
{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}
@@ -90,7 +90,7 @@
+ *ngFor="let datasourceControl of dataSettings.get('datasources').controls; let $index = index;"> {{$index + 1}}.
@@ -130,25 +130,15 @@
- + + + + +
{this.props.error}
+ + + + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardAceEditor); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx index 755b0f1bfb..1e931c6332 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx +++ b/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 { +export default ThingsboardBaseComponent => class

+ extends React.Component { constructor(props) { super(props); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx new file mode 100644 index 0000000000..74025e2b32 --- /dev/null +++ b/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 { + + 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 ( +

+
+
+
+
+ +
+ +
+ ); + } +} + +export default ThingsboardBaseComponent(ThingsboardColor); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx new file mode 100644 index 0000000000..552fd76843 --- /dev/null +++ b/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 { + + constructor(props) { + super(props); + this.onTidyCss = this.onTidyCss.bind(this); + } + + onTidyCss(css: string): string { + return css_beautify(css, {indent_size: 4}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardCss; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx new file mode 100644 index 0000000000..bbe1c50e53 --- /dev/null +++ b/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 { + + constructor(props) { + super(props); + this.onTidyHtml = this.onTidyHtml.bind(this); + } + + onTidyHtml(html: string): string { + return html_beautify(html, {indent_size: 4}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardHtml; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx new file mode 100644 index 0000000000..349d9c687b --- /dev/null +++ b/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 { + + 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 = ; + } else { + previewComponent =
No image selected
; + } + + return ( +
+ +
+
{previewComponent}
+
+ + + +
+ + {({getRootProps, getInputProps}) => ( +
+
Drop an image or click to select a file to upload.
+ +
+ )} +
+
+
+ ); + } +} + +export default ThingsboardBaseComponent(ThingsboardImage); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx new file mode 100644 index 0000000000..d724d5cf95 --- /dev/null +++ b/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 { + + 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 ( + + ); + } +} + +export default ThingsboardJavaScript; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx new file mode 100644 index 0000000000..d335836e21 --- /dev/null +++ b/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 { + + constructor(props) { + super(props); + this.onTidyJson = this.onTidyJson.bind(this); + } + + onTidyJson(json: string): string { + return js_beautify(json, {indent_size: 4}); + } + + render() { + return ( + + ); + } +} + +export default ThingsboardJson; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx index 0c6d7ef921..145c2aedb9 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx +++ b/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 { @@ -52,15 +53,15 @@ class ThingsboardSchemaForm extends React.Component { 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 { } } - 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 -
{this.props.info.GroupTitle}
{this.props.forms}
); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts b/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts index df58a1918c..d849cd8860 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts +++ b/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) { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts b/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts index a90dc47392..f53d52177e 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts +++ b/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?: { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss index 09676a37fc..ae6c9d5597 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss +++ b/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; + } + } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 63965492a6..02837ea5f9 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -283,11 +283,6 @@ export interface LegendData { data: Array; } -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}; - settings?: WidgetConfigSettings; + settings?: any; alarmSource?: Datasource; alarmSearchStatus?: AlarmSearchStatus; alarmsPollingInterval?: number; diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss index 21f5f58cdc..1cde9ee21a 100644 --- a/ui-ngx/src/scss/mixins.scss +++ b/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; +} diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 6cbfe8675f..2b96834b38 100644 --- a/ui-ngx/src/styles.scss +++ b/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%; + } + } }