From 6091f1a8d8806916b7635035a243c7c799ae52be Mon Sep 17 00:00:00 2001 From: Mirco Pizzichini Date: Wed, 3 Jul 2019 12:05:06 +0200 Subject: [PATCH 1/5] Add new widget 'multiple-widget' to bundle 'input_widgets' --- .../system/widget_bundles/input_widgets.json | 18 +- ui/src/app/api/widget.service.js | 3 +- ui/src/app/locale/locale.constant-en_US.json | 5 +- ui/src/app/locale/locale.constant-es_ES.json | 3 +- ui/src/app/locale/locale.constant-fr_FR.json | 3 +- ui/src/app/locale/locale.constant-it_IT.json | 3 +- .../app/widget/lib/multiple-input-widget.js | 259 ++++++++++++++++++ .../app/widget/lib/multiple-input-widget.scss | 55 ++++ .../widget/lib/multiple-input-widget.tpl.html | 55 ++++ 9 files changed, 397 insertions(+), 7 deletions(-) create mode 100644 ui/src/app/widget/lib/multiple-input-widget.js create mode 100644 ui/src/app/widget/lib/multiple-input-widget.scss create mode 100644 ui/src/app/widget/lib/multiple-input-widget.tpl.html diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 6dc7f2aad0..3c8ed7c938 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -196,6 +196,22 @@ "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } + }, + { + "alias": "update_multiple_attributes", + "name": "Update Multiple Attributes", + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Multiple input title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributesShared\": {\n \"title\": \"Attributes are 'shared' (default value is 'server')\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"attributesShared\",\n \"showResultMessage\"\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } } ] -} \ No newline at end of file +} diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 4859f7f409..ea59130484 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -24,6 +24,7 @@ import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget' import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget'; import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget'; import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator'; +import thingsboardMultipleInputWidget from '../widget/lib/multiple-input-widget'; import thingsboardRpcWidgets from '../widget/lib/rpc'; @@ -49,7 +50,7 @@ import thingsboardUtils from '../common/utils.service'; export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget, - thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget]) + thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget]) .factory('widgetService', WidgetService) .name; diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 9e2d3041e3..755c4e7671 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -49,7 +49,8 @@ "import": "Import", "export": "Export", "share-via": "Share via {{provider}}", - "continue": "Continue" + "continue": "Continue", + "discard-changes": "Discard Changes" }, "aggregation": { "aggregation": "Aggregation", @@ -1694,4 +1695,4 @@ "cs_CZ": "Czech" } } -} \ No newline at end of file +} diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json index 0d76e8b566..78e09eccb0 100644 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ b/ui/src/app/locale/locale.constant-es_ES.json @@ -48,7 +48,8 @@ "paste-reference": "Pegar referencia", "import": "Importar", "export": "Exportar", - "share-via": "Compartir via {{provider}}" + "share-via": "Compartir via {{provider}}", + "discard-changes": "Cancelar los cambios" }, "aggregation": { "aggregation": "Agregación", diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json index 036e10efa4..be887bcf89 100644 --- a/ui/src/app/locale/locale.constant-fr_FR.json +++ b/ui/src/app/locale/locale.constant-fr_FR.json @@ -48,7 +48,8 @@ "undo": "Annuler", "update": "mise à jour", "view": "Afficher", - "yes": "Oui" + "yes": "Oui", + "discard-changes": "Annuler les modifications" }, "admin": { "base-url": "URL de base", diff --git a/ui/src/app/locale/locale.constant-it_IT.json b/ui/src/app/locale/locale.constant-it_IT.json index b53bdbf558..8606173e92 100644 --- a/ui/src/app/locale/locale.constant-it_IT.json +++ b/ui/src/app/locale/locale.constant-it_IT.json @@ -48,7 +48,8 @@ "paste-reference": "Incolla riferimento", "import": "Importa", "export": "Esporta", - "share-via": "Condividi con {{provider}}" + "share-via": "Condividi con {{provider}}", + "discard-changes": "Annulla le modifiche" }, "aggregation": { "aggregation": "Aggregazione", diff --git a/ui/src/app/widget/lib/multiple-input-widget.js b/ui/src/app/widget/lib/multiple-input-widget.js new file mode 100644 index 0000000000..54f163dc92 --- /dev/null +++ b/ui/src/app/widget/lib/multiple-input-widget.js @@ -0,0 +1,259 @@ +/* + * 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 './multiple-input-widget.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import multipleInputWidgetTemplate from './multiple-input-widget.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.widgets.multipleInputWidget', []) + .directive('tbMultipleInputWidget', MultipleInputWidget) + .name; + +/*@ngInject*/ +function MultipleInputWidget() { + return { + restrict: "E", + scope: true, + bindToController: { + formId: '=', + ctx: '=' + }, + controller: MultipleInputWidgetController, + controllerAs: 'vm', + templateUrl: multipleInputWidgetTemplate + }; +} + +/*@ngInject*/ +function MultipleInputWidgetController($q, $scope, attributeService, toast, types, utils) { + var vm = this; + + vm.dataKeyDetected = false; + vm.hasAnyChange = false; + vm.entityDetected = false; + vm.isValidParameter = true; + vm.message = 'No entity selected'; + + vm.rows = []; + vm.rowIndex = 0; + + vm.datasources = null; + + vm.cellStyle = cellStyle; + vm.discardAll = discardAll; + vm.inputChanged = inputChanged; + vm.postData = postData; + + $scope.$watch('vm.ctx', function() { + if (vm.ctx && vm.ctx.defaultSubscription) { + vm.settings = vm.ctx.settings; + vm.widgetConfig = vm.ctx.widgetConfig; + vm.subscription = vm.ctx.defaultSubscription; + vm.datasources = vm.subscription.datasources; + initializeConfig(); + updateDatasources(); + } + }); + + $scope.$on('multiple-input-data-updated', function(event, formId) { + if (vm.formId == formId) { + updateRowData(vm.subscription.data); + $scope.$digest(); + } + }); + + function defaultStyle() { + return {}; + } + + function cellStyle(key) { + var style = {}; + if (key) { + var styleInfo = vm.stylesInfo[key.label]; + var value = key.currentValue; + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } else { + style = defaultStyle(); + } + } + return style; + } + + function discardAll() { + for (var r = 0; r < vm.rows.length; r++) { + var row = vm.rows[r]; + for (var d = 0; d < row.data.length; d++ ) { + row.data[d].currentValue = row.data[d].originalValue; + } + } + vm.hasAnyChange = false; + } + + function inputChanged() { + var newValue = false; + for (var r = 0; r < vm.rows.length; r++) { + var row = vm.rows[r]; + for (var d = 0; d < row.data.length; d++ ) { + if (!row.data[d].currentValue) { + return; + } + if (row.data[d].currentValue !== row.data[d].originalValue) { + newValue = true; + } + } + } + vm.hasAnyChange = newValue; + } + + function postData() { + var promises = []; + for (var r = 0; r < vm.rows.length; r++) { + var row = vm.rows[r]; + var datasource = row.datasource; + var attributes = []; + var newValues = false; + + for (var d = 0; d < row.data.length; d++ ) { + if (row.data[d].currentValue !== row.data[d].originalValue) { + attributes.push({ + key : row.data[d].name, + value : row.data[d].currentValue, + }); + newValues = true; + } + } + + if (newValues) { + promises.push(attributeService.saveEntityAttributes( + datasource.entityType, + datasource.entityId, + vm.attributeScope, + attributes)); + } + } + + if (promises.length) { + $q.all(promises).then( + function success() { + for (var d = 0; d < row.data.length; d++ ) { + row.data[d].originalValue = row.data[d].currentValue; + } + vm.hasAnyChange = false; + if (vm.settings.showResultMessage) { + toast.showSuccess('Update successful', 1000, angular.element(vm.ctx.$container), 'bottom left'); + } + }, + function fail() { + if (vm.settings.showResultMessage) { + toast.showError('Update failed', angular.element(vm.ctx.$container), 'bottom left'); + } + } + ); + } + } + + function initializeConfig() { + + if (vm.settings.widgetTitle && vm.settings.widgetTitle.length) { + vm.widgetTitle = utils.customTranslation(vm.settings.widgetTitle, vm.settings.widgetTitle); + } else { + vm.widgetTitle = vm.ctx.widgetConfig.title; + } + + vm.ctx.widgetTitle = vm.widgetTitle; + + vm.attributeScope = vm.settings.attributesShared ? types.attributesScope.shared.value : types.attributesScope.server.value; + } + + function updateDatasources() { + + vm.stylesInfo = {}; + vm.rows = []; + vm.rowIndex = 0; + + if (vm.datasources) { + vm.entityDetected = true; + for (var ds = 0; ds < vm.datasources.length; ds++) { + var row = {}; + var datasource = vm.datasources[ds]; + row.datasource = datasource; + row.data = []; + if (datasource.dataKeys) { + vm.dataKeyDetected = true; + for (var a = 0; a < datasource.dataKeys.length; a++ ) { + var dataKey = datasource.dataKeys[a]; + + if (dataKey.units) { + dataKey.label += ' (' + dataKey.units + ')'; + } + + var keySettings = dataKey.settings; + if (keySettings.inputTypeNumber) { + keySettings.inputType = 'number'; + } else { + keySettings.inputType = 'text'; + } + + var cellStyleFunction = null; + var useCellStyleFunction = false; + + if (keySettings.useCellStyleFunction === true) { + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { + try { + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); + useCellStyleFunction = true; + } catch (e) { + cellStyleFunction = null; + useCellStyleFunction = false; + } + } + } + + vm.stylesInfo[dataKey.label] = { + useCellStyleFunction: useCellStyleFunction, + cellStyleFunction: cellStyleFunction + }; + + row.data.push(dataKey); + } + vm.rows.push(row); + } + } + } + } + + function updateRowData(data) { + var dataIndex = 0; + for (var r = 0; r < vm.rows.length; r++) { + var row = vm.rows[r]; + for (var d = 0; d < row.data.length; d++ ) { + var keyData = data[dataIndex++].data; + if (keyData && keyData.length && keyData[0].length > 1) { + row.data[d].currentValue = row.data[d].originalValue = keyData[0][1]; + } + } + } + } + +} diff --git a/ui/src/app/widget/lib/multiple-input-widget.scss b/ui/src/app/widget/lib/multiple-input-widget.scss new file mode 100644 index 0000000000..ed8ae1c907 --- /dev/null +++ b/ui/src/app/widget/lib/multiple-input-widget.scss @@ -0,0 +1,55 @@ +/** + * 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. + */ +.tb-multiple-input { + height: 100%; + + .md-button.md-icon-button { + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0 !important; + margin: 0; + line-height: 20px; + } + + .md-icon-button md-icon { + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + font-size: 20px; + + &:not([disabled]) { + color: #f66; + } + } +} + +md-toast { + min-width: 0; + + .md-toast-content { + font-size: 14px !important; + } +} + +.footer { + position: absolute; + bottom: 0; + left: 0; + width: 100%; +} diff --git a/ui/src/app/widget/lib/multiple-input-widget.tpl.html b/ui/src/app/widget/lib/multiple-input-widget.tpl.html new file mode 100644 index 0000000000..adee81457c --- /dev/null +++ b/ui/src/app/widget/lib/multiple-input-widget.tpl.html @@ -0,0 +1,55 @@ + +
+
+
+
+ + + + {{key.settings.icon}} + + + +
+
+ +
+
+ No attribute is selected +
+
+ Timeseries parameter cannot be used in this widget +
+
+ +
\ No newline at end of file From dd03bf30d88eea55564914d1f0503456cb360e5c Mon Sep 17 00:00:00 2001 From: Mirco Pizzichini Date: Wed, 3 Jul 2019 12:27:58 +0200 Subject: [PATCH 2/5] Add new React component 'ThingsboardIcon', a form to select an icon from datakey settings --- ui/src/app/components/json-form.directive.js | 26 +++- .../app/components/react/json-form-array.jsx | 2 +- .../components/react/json-form-fieldset.jsx | 2 +- .../app/components/react/json-form-icon.jsx | 134 ++++++++++++++++++ .../app/components/react/json-form-react.jsx | 1 + .../react/json-form-schema-form.jsx | 13 +- 6 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 ui/src/app/components/react/json-form-icon.jsx diff --git a/ui/src/app/components/json-form.directive.js b/ui/src/app/components/json-form.directive.js index f653f576ed..84f62f939a 100644 --- a/ui/src/app/components/json-form.directive.js +++ b/ui/src/app/components/json-form.directive.js @@ -22,13 +22,17 @@ import ReactSchemaForm from './react/json-form-react.jsx'; import jsonFormTemplate from './json-form.tpl.html'; import { utils } from 'react-schema-form'; +import MaterialIconsDialogController from './material-icons-dialog.controller'; +import materialIconsDialogTemplate from './material-icons-dialog.tpl.html'; + export default angular.module('thingsboard.directives.jsonForm', []) .directive('tbJsonForm', JsonForm) + .controller('MaterialIconsDialogController', MaterialIconsDialogController) .value('ReactSchemaForm', ReactSchemaForm) .name; /*@ngInject*/ -function JsonForm($compile, $templateCache, $mdColorPicker) { +function JsonForm($compile, $templateCache, $mdColorPicker, $mdDialog, $document) { var linker = function (scope, element) { @@ -90,6 +94,9 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { onColorClick: function(event, key, val) { scope.showColorPicker(event, val); }, + onIconClick: function(event) { + scope.openIconDialog(event); + }, onToggleFullscreen: function() { scope.isFullscreen = !scope.isFullscreen; scope.formProps.isFullscreen = scope.isFullscreen; @@ -123,6 +130,23 @@ function JsonForm($compile, $templateCache, $mdColorPicker) { }); } + scope.openIconDialog = function(event) { + $mdDialog.show({ + controller: 'MaterialIconsDialogController', + controllerAs: 'vm', + templateUrl: materialIconsDialogTemplate, + parent: angular.element($document[0].body), + locals: {icon: scope.icon}, + multiple: true, + fullscreen: true, + targetEvent: event + }).then(function (icon) { + if (event.data && event.data.onValueChanged) { + event.data.onValueChanged(icon); + } + }); + } + scope.onFullscreenChanged = function() {} scope.validate = function(){ diff --git a/ui/src/app/components/react/json-form-array.jsx b/ui/src/app/components/react/json-form-array.jsx index 46f6457b0f..839b55a4fb 100644 --- a/ui/src/app/components/react/json-form-array.jsx +++ b/ui/src/app/components/react/json-form-array.jsx @@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component { } let forms = this.props.form.items.map(function(form, index){ var copy = this.copyWithIndex(form, i); - return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); + return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); }.bind(this)); arrays.push(
  • diff --git a/ui/src/app/components/react/json-form-fieldset.jsx b/ui/src/app/components/react/json-form-fieldset.jsx index 5a0f94017c..3c99ca4be7 100644 --- a/ui/src/app/components/react/json-form-fieldset.jsx +++ b/ui/src/app/components/react/json-form-fieldset.jsx @@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component { render() { let forms = this.props.form.items.map(function(form, index){ - return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); + return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder); }.bind(this)); return ( diff --git a/ui/src/app/components/react/json-form-icon.jsx b/ui/src/app/components/react/json-form-icon.jsx new file mode 100644 index 0000000000..44cf321fe0 --- /dev/null +++ b/ui/src/app/components/react/json-form-icon.jsx @@ -0,0 +1,134 @@ +/* + * 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 $ from 'jquery'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import ThingsboardBaseComponent from './json-form-base-component.jsx'; +import reactCSS from 'reactcss'; +import TextField from 'material-ui/TextField'; +import IconButton from 'material-ui/IconButton'; + +class ThingsboardIcon extends React.Component { + + constructor(props) { + super(props); + this.onValueChanged = this.onValueChanged.bind(this); + this.onIconClick = this.onIconClick.bind(this); + this.onClear = this.onClear.bind(this); + var icon = props.value ? props.value : ''; + this.state = { + icon: icon + }; + } + + componentDidMount() { + var node = ReactDOM.findDOMNode(this); + var iconContainer = $(node).children('#icon-container'); + iconContainer.click(this, function(event) { + event.data.onIconClick(event); + }); + } + + componentWillUnmount () { + var node = ReactDOM.findDOMNode(this); + var iconContainer = $(node).children('#icon-container'); + iconContainer.off( "click" ); + } + + onValueChanged(value) { + var icon = value; + + this.setState({ + icon: value + }) + this.props.onChange(this.props.form.key, value); + } + + onIconClick(event) { + this.props.onIconClick(event); + } + + onClear(event) { + if (event) { + event.stopPropagation(); + } + this.onValueChanged(''); + } + + render() { + + const styles = reactCSS({ + 'default': { + clear: { + marginTop: '15px' + }, + container: { + display: 'flex' + }, + icon: { + display: 'inline-block', + marginRight: '10px', + marginTop: '16px', + marginBottom: 'auto', + cursor: 'pointer', + border: 'solid 1px rgba(0, 0, 0, .27)' + }, + iconContainer: { + display: 'flex', + width: '100%' + }, + iconText: { + display: 'inline-block', + width: '100%' + }, + }, + }); + + var fieldClass = "tb-field"; + if (this.props.form.required) { + fieldClass += " tb-required"; + } + if (this.state.focused) { + fieldClass += " tb-focused"; + } + + var pickedIcon = 'more_horiz'; + if (this.state.icon != '') { + pickedIcon = this.state.icon; + } + + return ( +
    +
    + + {pickedIcon} + + +
    + clear +
    + ); + } +} + +export default ThingsboardBaseComponent(ThingsboardIcon); diff --git a/ui/src/app/components/react/json-form-react.jsx b/ui/src/app/components/react/json-form-react.jsx index ad73359199..3c11f5f850 100644 --- a/ui/src/app/components/react/json-form-react.jsx +++ b/ui/src/app/components/react/json-form-react.jsx @@ -52,6 +52,7 @@ ReactSchemaForm.propTypes = { option: React.PropTypes.object, onModelChange: React.PropTypes.func, onColorClick: React.PropTypes.func, + onIconClick: React.PropTypes.func, onToggleFullscreen: React.PropTypes.func } diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx index 8d9ec3e874..35446c4637 100644 --- a/ui/src/app/components/react/json-form-schema-form.jsx +++ b/ui/src/app/components/react/json-form-schema-form.jsx @@ -32,6 +32,7 @@ import ThingsboardImage from './json-form-image.jsx'; import ThingsboardCheckbox from './json-form-checkbox.jsx'; import Help from 'react-schema-form/lib/Help'; import ThingsboardFieldSet from './json-form-fieldset.jsx'; +import ThingsboardIcon from './json-form-icon.jsx'; import _ from 'lodash'; @@ -58,11 +59,13 @@ class ThingsboardSchemaForm extends React.Component { 'css': ThingsboardCss, 'color': ThingsboardColor, 'rc-select': ThingsboardRcSelect, - 'fieldset': ThingsboardFieldSet + 'fieldset': ThingsboardFieldSet, + 'icon': ThingsboardIcon }; this.onChange = this.onChange.bind(this); this.onColorClick = this.onColorClick.bind(this); + this.onIconClick = this.onIconClick.bind(this); this.onToggleFullscreen = this.onToggleFullscreen.bind(this); this.hasConditions = false; } @@ -84,7 +87,7 @@ class ThingsboardSchemaForm extends React.Component { } - builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) { + builder(form, model, index, onChange, onColorClick, onIconClick, onToggleFullscreen, mapper) { var type = form.type; let Field = this.mapper[type]; if(!Field) { @@ -97,7 +100,7 @@ class ThingsboardSchemaForm extends React.Component { return null; } } - return + return } createSchema(theForm) { @@ -107,7 +110,7 @@ class ThingsboardSchemaForm extends React.Component { mapper = _.merge(this.mapper, this.props.mapper); } let forms = merged.map(function(form, index) { - return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper); + return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onIconClick, this.onToggleFullscreen, mapper); }.bind(this)); let formClass = 'SchemaForm'; @@ -158,4 +161,4 @@ class ThingsboardSchemaGroup extends React.Component{
    {this.props.forms}
    ); } -} \ No newline at end of file +} From 01ba8f900b60d8dc2a811f3713e6bec21173196c Mon Sep 17 00:00:00 2001 From: Mirco Pizzichini Date: Thu, 4 Jul 2019 08:51:57 +0200 Subject: [PATCH 3/5] Add onIconClick method --- ui/src/app/components/react/json-form-schema-form.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx index 35446c4637..13c4c97a21 100644 --- a/ui/src/app/components/react/json-form-schema-form.jsx +++ b/ui/src/app/components/react/json-form-schema-form.jsx @@ -82,6 +82,10 @@ class ThingsboardSchemaForm extends React.Component { this.props.onColorClick(event, key, val); } + onIconClick(event) { + this.props.onIconClick(event); + } + onToggleFullscreen() { this.props.onToggleFullscreen(); } @@ -161,4 +165,4 @@ class ThingsboardSchemaGroup extends React.Component{
    {this.props.forms}
    ); } -} +} From fc6e219b9e51d5e44889231443aa5384faaf059c Mon Sep 17 00:00:00 2001 From: Mirco Pizzichini Date: Tue, 30 Jul 2019 14:33:39 +0200 Subject: [PATCH 4/5] Add the possibility to set a datakey as readonly --- .../main/data/json/system/widget_bundles/input_widgets.json | 2 +- ui/src/app/widget/lib/multiple-input-widget.tpl.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json index 3c8ed7c938..cec243c975 100644 --- a/application/src/main/data/json/system/widget_bundles/input_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json @@ -209,7 +209,7 @@ "templateCss": "", "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Multiple input title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributesShared\": {\n \"title\": \"Attributes are 'shared' (default value is 'server')\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"attributesShared\",\n \"showResultMessage\"\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"readOnly\",\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } } diff --git a/ui/src/app/widget/lib/multiple-input-widget.tpl.html b/ui/src/app/widget/lib/multiple-input-widget.tpl.html index adee81457c..6fb935bea8 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.tpl.html +++ b/ui/src/app/widget/lib/multiple-input-widget.tpl.html @@ -25,6 +25,7 @@ {{key.settings.icon}} - \ No newline at end of file + From 3f76ab12fb5dc32f61b2d7ada911dbed9fbe9924 Mon Sep 17 00:00:00 2001 From: Mirco Pizzichini Date: Thu, 1 Aug 2019 10:18:12 +0200 Subject: [PATCH 5/5] Add further datakey settings: tooltip, text color, min-max range, margins, mandatory --- .../app/widget/lib/multiple-input-widget.js | 33 +++++++++++++++++-- .../app/widget/lib/multiple-input-widget.scss | 7 ---- .../widget/lib/multiple-input-widget.tpl.html | 32 ++++++++++++------ 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/ui/src/app/widget/lib/multiple-input-widget.js b/ui/src/app/widget/lib/multiple-input-widget.js index 54f163dc92..ad8972ff7d 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.js +++ b/ui/src/app/widget/lib/multiple-input-widget.js @@ -56,6 +56,7 @@ function MultipleInputWidgetController($q, $scope, attributeService, toast, type vm.datasources = null; vm.cellStyle = cellStyle; + vm.textColor = textColor; vm.discardAll = discardAll; vm.inputChanged = inputChanged; vm.postData = postData; @@ -82,7 +83,7 @@ function MultipleInputWidgetController($q, $scope, attributeService, toast, type return {}; } - function cellStyle(key) { + function cellStyle(key, rowIndex, firstKey, lastKey) { var style = {}; if (key) { var styleInfo = vm.stylesInfo[key.label]; @@ -97,6 +98,33 @@ function MultipleInputWidgetController($q, $scope, attributeService, toast, type style = defaultStyle(); } } + if (vm.settings.rowMargin) { + if (angular.isUndefined(style.marginTop) && rowIndex != 0) { + style.marginTop = (vm.settings.rowMargin / 2) + 'px'; + } + if (angular.isUndefined(style.marginBottom)) { + style.marginBottom = (vm.settings.rowMargin / 2) + 'px'; + } + } + if (vm.settings.columnMargin) { + if (angular.isUndefined(style.marginLeft) && !firstKey) { + style.marginLeft = (vm.settings.columnMargin / 2) + 'px'; + } + if (angular.isUndefined(style.marginRight) && !lastKey) { + style.marginRight = (vm.settings.columnMargin / 2) + 'px'; + } + } + return style; + } + + function textColor(key) { + var style = {}; + if (key) { + var styleInfo = vm.stylesInfo[key.label]; + if (styleInfo.color) { + style = { color: styleInfo.color }; + } + } return style; } @@ -232,7 +260,8 @@ function MultipleInputWidgetController($q, $scope, attributeService, toast, type vm.stylesInfo[dataKey.label] = { useCellStyleFunction: useCellStyleFunction, - cellStyleFunction: cellStyleFunction + cellStyleFunction: cellStyleFunction, + color: keySettings.color }; row.data.push(dataKey); diff --git a/ui/src/app/widget/lib/multiple-input-widget.scss b/ui/src/app/widget/lib/multiple-input-widget.scss index ed8ae1c907..7e16b3f967 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.scss +++ b/ui/src/app/widget/lib/multiple-input-widget.scss @@ -46,10 +46,3 @@ md-toast { font-size: 14px !important; } } - -.footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; -} diff --git a/ui/src/app/widget/lib/multiple-input-widget.tpl.html b/ui/src/app/widget/lib/multiple-input-widget.tpl.html index 6fb935bea8..1263919825 100644 --- a/ui/src/app/widget/lib/multiple-input-widget.tpl.html +++ b/ui/src/app/widget/lib/multiple-input-widget.tpl.html @@ -15,22 +15,34 @@ limitations under the License. --> -
    +
    -
    -
    - - - +
    +
    + + + + + + {{key.settings.icon}} - +
    +
    Value must be greater than {{key.settings.min}}
    +
    Value must be lower than {{key.settings.max}}
    +
    This field is required
    +
    @@ -44,12 +56,12 @@ Timeseries parameter cannot be used in this widget
    -