Add new widget 'multiple-widget' to bundle 'input_widgets'

This commit is contained in:
Mirco Pizzichini 2019-07-03 12:05:06 +02:00
parent a568564ed3
commit 6091f1a8d8
9 changed files with 397 additions and 7 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 \"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 \"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

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

View File

@ -48,7 +48,8 @@
"paste-reference": "Pegar referencia",
"import": "Importar",
"export": "Exportar",
"share-via": "Compartir via {{provider}}"
"share-via": "Compartir via {{provider}}",
"discard-changes": "Cancelar los cambios"
},
"aggregation": {
"aggregation": "Agregación",

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,259 @@
/*
* 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.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) {
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();
}
}
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
};
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,55 @@
/**
* 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;
}
}
.footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
}

View File

@ -0,0 +1,55 @@
<!--
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)">
<div style="padding: 0 8px; margin: auto 0;">
<div ng-show="vm.entityDetected" layout="row" flex ng-repeat="row in vm.rows track by $index">
<div layout="column" flex ng-repeat="key in row.data track by $index">
<md-input-container class="md-icon-float" ng-style="vm.cellStyle(key)">
<label>{{key.label}}</label>
<md-icon class="material-icons" ng-if="key.settings.icon">
{{key.settings.icon}}
</md-icon>
<input name="key.name"
ng-model="key.currentValue"
type="{{key.settings.inputType}}"
step="{{key.settings.step}}"
md-select-on-focus
ng-change="vm.inputChanged()">
</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="footer 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.discard-changes' | 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" ng-click="vm.isFocused = false">
{{ 'action.save' | translate }}
</md-button>
</div>
</form>