15 changed files with 604 additions and 13 deletions
@ -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); |
||||
@ -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]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue