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