From 6091f1a8d8806916b7635035a243c7c799ae52be Mon Sep 17 00:00:00 2001 From: Mirco Pizzichini Date: Wed, 3 Jul 2019 12:05:06 +0200 Subject: [PATCH] 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