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..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 @@ -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 \"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\":{}}" + } } ] -} \ 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/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..13c4c97a21 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; } @@ -79,12 +82,16 @@ class ThingsboardSchemaForm extends React.Component { this.props.onColorClick(event, key, val); } + onIconClick(event) { + this.props.onIconClick(event); + } + onToggleFullscreen() { this.props.onToggleFullscreen(); } - 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 +104,7 @@ class ThingsboardSchemaForm extends React.Component { return null; } } - return + return } createSchema(theForm) { @@ -107,7 +114,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 +165,4 @@ class ThingsboardSchemaGroup extends React.Component{
    {this.props.forms}
    ); } -} \ No newline at end of file +} diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 689bd149ef..af49a7b0b3 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", diff --git a/ui/src/app/locale/locale.constant-es_ES.json b/ui/src/app/locale/locale.constant-es_ES.json index b36b41616f..fbda6661af 100644 --- a/ui/src/app/locale/locale.constant-es_ES.json +++ b/ui/src/app/locale/locale.constant-es_ES.json @@ -49,6 +49,7 @@ "import": "Importar", "export": "Exportar", "share-via": "Compartir vía {{provider}}", + "discard-changes": "Cancelar los cambios", "continue": "Continuar" }, "aggregation": { diff --git a/ui/src/app/locale/locale.constant-fr_FR.json b/ui/src/app/locale/locale.constant-fr_FR.json index 5dc4dfbc13..9713de7626 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 71b14747d7..f2f04c47a9 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..ad8972ff7d --- /dev/null +++ b/ui/src/app/widget/lib/multiple-input-widget.js @@ -0,0 +1,288 @@ +/* + * 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.textColor = textColor; + 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, rowIndex, firstKey, lastKey) { + 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(); + } + } + 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; + } + + 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, + color: keySettings.color + }; + + 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..7e16b3f967 --- /dev/null +++ b/ui/src/app/widget/lib/multiple-input-widget.scss @@ -0,0 +1,48 @@ +/** + * 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; + } +} 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..1263919825 --- /dev/null +++ b/ui/src/app/widget/lib/multiple-input-widget.tpl.html @@ -0,0 +1,68 @@ + +
    +
    +
    +
    + + + + + + + {{key.settings.icon}} + + +
    +
    Value must be greater than {{key.settings.min}}
    +
    Value must be lower than {{key.settings.max}}
    +
    This field is required
    +
    +
    +
    +
    + +
    +
    + No attribute is selected +
    +
    + Timeseries parameter cannot be used in this widget +
    +
    +
    + + {{ 'action.undo' | translate }} + + + {{ 'action.save' | translate }} + +
    +