Browse Source

Merge branch 'mircopz-feature/new-multiple-input-widget'

pull/1941/head
Igor Kulikov 7 years ago
parent
commit
66f74e961e
  1. 18
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 3
      ui/src/app/api/widget.service.js
  3. 26
      ui/src/app/components/json-form.directive.js
  4. 2
      ui/src/app/components/react/json-form-array.jsx
  5. 2
      ui/src/app/components/react/json-form-fieldset.jsx
  6. 134
      ui/src/app/components/react/json-form-icon.jsx
  7. 1
      ui/src/app/components/react/json-form-react.jsx
  8. 17
      ui/src/app/components/react/json-form-schema-form.jsx
  9. 3
      ui/src/app/locale/locale.constant-en_US.json
  10. 1
      ui/src/app/locale/locale.constant-es_ES.json
  11. 3
      ui/src/app/locale/locale.constant-fr_FR.json
  12. 3
      ui/src/app/locale/locale.constant-it_IT.json
  13. 288
      ui/src/app/widget/lib/multiple-input-widget.js
  14. 48
      ui/src/app/widget/lib/multiple-input-widget.scss
  15. 68
      ui/src/app/widget/lib/multiple-input-widget.tpl.html

18
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": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>",
"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\":{}}"
}
}
]
}
}

3
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;

26
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(){

2
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(
<li key={keys[i]} className="list-group-item">

2
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 (

134
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 (
<div style={ styles.container }>
<div id="icon-container" style={ styles.iconContainer }>
<IconButton iconClassName="material-icons" style={ styles.icon }>
{pickedIcon}
</IconButton>
<TextField
className={fieldClass}
floatingLabelText={this.props.form.title}
hintText={this.props.form.placeholder}
errorText={this.props.error}
value={this.state.icon}
disabled={this.props.form.readonly}
style={ styles.iconText } />
</div>
<IconButton iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardIcon);

1
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
}

17
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 <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
}
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{
<div style={{padding: '20px'}} className={this.state.showGroup?"":"invisible"}>{this.props.forms}</div>
</section>);
}
}
}

3
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",

1
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": {

3
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",

3
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",

288
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];
}
}
}
}
}

48
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;
}
}

68
ui/src/app/widget/lib/multiple-input-widget.tpl.html

@ -0,0 +1,68 @@
<!--
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.
-->
<form class="tb-multiple-input" name="multipleInputForm" ng-submit="vm.postData($event)" novalidate>
<div style="padding: 0 8px; margin: auto 0;">
<div ng-show="vm.entityDetected" layout="row" flex ng-repeat="row in vm.rows" ng-init="rowIndex=$index">
<div layout="column" flex ng-repeat="key in row.data track by $index" ng-init="keyIndex=$index">
<md-tooltip class="tb-tooltip-multiline" ng-if="key.settings.tooltipMessage && key.settings.tooltipMessage.length" md-direction="left">
<span ng-bind-html="key.settings.tooltipMessage"></span>
</md-tooltip>
<md-input-container class="md-block" ng-style="vm.cellStyle(key, rowIndex, $first, $last)">
<label ng-style="vm.textColor(key)">{{key.label}}</label>
<md-icon ng-style="vm.textColor(key)" class="material-icons" ng-if="key.settings.icon">
{{key.settings.icon}}
</md-icon>
<input name="value{{rowIndex}}{{keyIndex}}"
ng-style="vm.textColor(key)"
ng-disabled="key.settings.readOnly"
ng-model="key.currentValue"
min="{{key.settings.min}}"
max="{{key.settings.max}}"
ng-required="key.settings.required"
type="{{key.settings.inputType}}"
step="{{key.settings.step}}"
md-select-on-focus
ng-change="vm.inputChanged()">
<div ng-messages="multipleInputForm['value' + rowIndex + keyIndex].$error">
<div ng-message="min">Value must be greater than {{key.settings.min}}</div>
<div ng-message="max">Value must be lower than {{key.settings.max}}</div>
<div ng-message="required">This field is required</div>
</div>
</md-input-container>
</div>
</div>
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-hide="vm.entityDetected" ng-bind="vm.message"
></div>
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.dataKeyDetected">
No attribute is selected
</div>
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.isValidParameter">
Timeseries parameter cannot be used in this widget
</div>
</div>
<div class="md-padding" layout="row" layout-align="end center" ng-show="vm.entityDetected && vm.dataKeyDetected">
<md-button class="md-primary" ng-click="vm.discardAll()" style="max-height: 50px;margin-right:20px;" ng-disabled="!vm.hasAnyChange">
{{ 'action.undo' | translate }}
</md-button>
<md-button class="md-raised md-primary" type="submit" value="Submit" style="max-height: 50px;margin-right:20px;"
ng-disabled="!vm.hasAnyChange || multipleInputForm.$invalid" ng-click="vm.isFocused = false">
{{ 'action.save' | translate }}
</md-button>
</div>
</form>
Loading…
Cancel
Save