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

This commit is contained in:
Igor Kulikov 2019-08-16 12:36:09 +03:00
commit 66f74e961e
15 changed files with 604 additions and 13 deletions

View File

@ -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\":{}}"
}
}
]
}

View File

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

View File

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

View File

@ -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">

View File

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

View File

@ -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);

View File

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

View File

@ -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';

View File

@ -49,7 +49,8 @@
"import": "Import",
"export": "Export",
"share-via": "Share via {{provider}}",
"continue": "Continue"
"continue": "Continue",
"discard-changes": "Discard Changes"
},
"aggregation": {
"aggregation": "Aggregation",

View File

@ -49,6 +49,7 @@
"import": "Importar",
"export": "Exportar",
"share-via": "Compartir vía {{provider}}",
"discard-changes": "Cancelar los cambios",
"continue": "Continuar"
},
"aggregation": {

View File

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

View File

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

View File

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

View File

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

View File

@ -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>