diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
index f9d05d83f3..12ffd8b82d 100644
--- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
@@ -18,7 +18,7 @@
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\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 \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
}
},
{
@@ -34,8 +34,8 @@
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\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 \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
}
}
]
-}
\ No newline at end of file
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index afe929f6b1..0884148e4f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -244,8 +244,18 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
}
+ String failureMessage;
+ if (th != null) {
+ if (!StringUtils.isEmpty(th.getMessage())) {
+ failureMessage = th.getMessage();
+ } else {
+ failureMessage = th.getClass().getSimpleName();
+ }
+ } else {
+ failureMessage = null;
+ }
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE),
- msg, th != null ? th.getMessage() : null));
+ msg, failureMessage));
}
public void updateSelf(RuleNode self) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
index ad5f97ab48..8cf2859f5a 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -56,6 +56,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
/**
@@ -288,10 +289,10 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor getTimeseriesKeys(
+ @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
+ DeviceProfileId deviceProfileId;
+ if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
+ deviceProfileId = new DeviceProfileId(UUID.fromString(deviceProfileIdStr));
+ checkDeviceProfileId(deviceProfileId, Operation.READ);
+ } else {
+ deviceProfileId = null;
+ }
+
+ try {
+ return timeseriesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/deviceProfile/devices/keys/attributes", method = RequestMethod.GET)
+ @ResponseBody
+ public List getAttributesKeys(
+ @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
+ DeviceProfileId deviceProfileId;
+ if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
+ deviceProfileId = new DeviceProfileId(UUID.fromString(deviceProfileIdStr));
+ checkDeviceProfileId(deviceProfileId, Operation.READ);
+ } else {
+ deviceProfileId = null;
+ }
+
+ try {
+ return attributesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile", method = RequestMethod.POST)
@ResponseBody
@@ -113,8 +164,8 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
- public void deleteDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
- checkParameter("deviceProfileId", strDeviceProfileId);
+ public void deleteDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
+ checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
@@ -139,8 +190,8 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST)
@ResponseBody
- public DeviceProfile setDefaultDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
- checkParameter("deviceProfileId", strDeviceProfileId);
+ public DeviceProfile setDefaultDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
+ checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index c5afb06483..65b7039a71 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -186,6 +186,10 @@ public class ThingsboardInstallService {
systemDataLoaderService.updateSystemWidgets();
systemDataLoaderService.createOAuth2Templates();
break;
+ case "3.2.0":
+ log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
+ databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
+ break;
default:
throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java
index fd4ef96122..a8e229933f 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/TenantApiUsageState.java
@@ -181,7 +181,7 @@ public class TenantApiUsageState {
long threshold = getProfileThreshold(recordKey);
long warnThreshold = getProfileWarnThreshold(recordKey);
ApiUsageStateValue tmpValue;
- if (threshold == 0 || value < warnThreshold) {
+ if (threshold == 0 || value == 0 || value < warnThreshold) {
tmpValue = ApiUsageStateValue.ENABLED;
} else if (value < threshold) {
tmpValue = ApiUsageStateValue.WARNING;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index 20c00dcb09..b0aabaea08 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -421,6 +421,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.error("Failed updating schema!!!", e);
}
break;
+ case "3.2.0":
+ try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+ log.info("Updating schema ...");
+ try {
+ conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_device_device_profile_id ON device(tenant_id, device_profile_id);");
+ conn.createStatement().execute("ALTER TABLE dashboard ALTER COLUMN configuration TYPE varchar;");
+ conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002001;");
+ } catch (Exception e) {
+ log.error("Failed updating schema!!!", e);
+ }
+ log.info("Schema updated.");
+ }
+ break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java
index c6cea69540..47b677cb14 100644
--- a/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java
+++ b/application/src/main/java/org/thingsboard/server/service/sms/aws/AwsSmsSender.java
@@ -20,6 +20,7 @@ import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClient;
+import com.amazonaws.services.sns.model.MessageAttributeValue;
import com.amazonaws.services.sns.model.PublishRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -28,9 +29,20 @@ import org.thingsboard.rule.engine.api.sms.exception.SmsException;
import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
import org.thingsboard.server.service.sms.AbstractSmsSender;
+import java.util.HashMap;
+import java.util.Map;
+
@Slf4j
public class AwsSmsSender extends AbstractSmsSender {
+ private static final Map SMS_ATTRIBUTES = new HashMap<>();
+
+ static {
+ SMS_ATTRIBUTES.put("AWS.SNS.SMS.SMSType", new MessageAttributeValue()
+ .withStringValue("Transactional")
+ .withDataType("String"));
+ }
+
private AmazonSNS snsClient;
public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
@@ -51,6 +63,7 @@ public class AwsSmsSender extends AbstractSmsSender {
message = this.prepareMessage(message);
try {
PublishRequest publishRequest = new PublishRequest()
+ .withMessageAttributes(SMS_ATTRIBUTES)
.withPhoneNumber(numberTo)
.withMessage(message);
this.snsClient.publish(publishRequest);
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 06661d9b58..791b032352 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -64,9 +64,9 @@ server:
# Minimum value of the server side RPC timeout. May override value provided in the REST API call.
# Since 2.5 migration to queues, the RPC delay depends on the size of the pending messages in the queue,
# so default UI parameter of 500ms may not be sufficient for loaded environments.
- min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
+ min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
# Default value of the server side RPC timeout.
- default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
+ default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
# Zookeeper connection parameters. Used for service discovery.
zk:
@@ -522,7 +522,7 @@ transport:
# Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check)
max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}"
client_side_rpc:
- timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}"
+ timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}"
# Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property)
api_enabled: "${TB_TRANSPORT_API_ENABLED:true}"
# Local HTTP transport parameters
@@ -595,7 +595,7 @@ queue:
linger.ms: "${TB_KAFKA_LINGER_MS:1}"
buffer.memory: "${TB_BUFFER_MEMORY:33554432}"
replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}"
- max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:0}"
+ max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}"
max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}"
@@ -695,8 +695,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
- # JS response auto commit interval
- response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
index 3af4737f06..2e5c895f84 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.attributes;
import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -38,4 +39,7 @@ public interface AttributesService {
ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes);
ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys);
+
+ List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
+
}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
index 0e11aedaf5..dbfbe3d0f5 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.timeseries;
import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
@@ -47,4 +48,6 @@ public interface TimeseriesService {
ListenableFuture> removeLatest(TenantId tenantId, EntityId entityId, Collection keys);
ListenableFuture> removeAllLatest(TenantId tenantId, EntityId entityId);
+
+ List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
index 0b257d9d6e..2df7a10320 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -195,11 +195,15 @@ public final class TbMsg implements Serializable {
}
public TbMsg copyWithRuleChainId(RuleChainId ruleChainId) {
- return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, null, this.ruleNodeExecCounter.get(), callback);
+ return copyWithRuleChainId(ruleChainId, this.id);
}
- public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
- return new TbMsg(this.queueName, this.id, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ruleNodeExecCounter.get(), callback);
+ public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) {
+ return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, null, this.ruleNodeExecCounter.get(), callback);
+ }
+
+ public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) {
+ return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ruleNodeExecCounter.get(), callback);
}
public TbMsgCallback getCallback() {
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
index bee293bd6d..ba8e86a495 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java
@@ -42,7 +42,7 @@ public class TbKafkaAdmin implements TbQueueAdmin {
private final short replicationFactor;
public TbKafkaAdmin(TbKafkaSettings settings, Map topicConfigs) {
- client = AdminClient.create(settings.toProps());
+ client = AdminClient.create(settings.toAdminProps());
this.topicConfigs = topicConfigs;
try {
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
index 90b560d0fc..e664a63c02 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
@@ -45,24 +45,14 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue
@Builder
private TbKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder decoder,
String clientId, String groupId, String topic,
- boolean autoCommit, int autoCommitIntervalMs,
TbQueueAdmin admin) {
super(topic);
- Properties props = settings.toProps();
+ Properties props = settings.toConsumerProps();
props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId);
if (groupId != null) {
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
}
- if (settings.getMaxPollIntervalMs() > 0) {
- props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, settings.getMaxPollIntervalMs());
- }
- props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, settings.getMaxPollRecords());
- props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, settings.getMaxPartitionFetchBytes());
- props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, settings.getFetchMaxBytes());
- props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, autoCommit);
- props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs);
- props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
- props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
+
this.admin = admin;
this.consumer = new KafkaConsumer<>(props);
this.decoder = decoder;
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
index 4f26f51da7..e154c8040c 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java
@@ -55,9 +55,8 @@ public class TbKafkaProducerTemplate implements TbQueuePro
@Builder
private TbKafkaProducerTemplate(TbKafkaSettings settings, String defaultTopic, String clientId, TbQueueAdmin admin) {
- Properties props = settings.toProps();
- props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
- props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
+ Properties props = settings.toProducerProps();
+
if (!StringUtils.isEmpty(clientId)) {
props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
index 15057d64d3..b5ba1931be 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
@@ -19,9 +19,11 @@ import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.CommonClientConfigs;
+import org.apache.kafka.clients.admin.AdminClientConfig;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -32,7 +34,7 @@ import java.util.Properties;
* Created by ashvayka on 25.09.18.
*/
@Slf4j
-@ConditionalOnExpression("'${queue.type:null}'=='kafka'")
+@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka")
@ConfigurationProperties(prefix = "queue.kafka")
@Component
public class TbKafkaSettings {
@@ -60,19 +62,15 @@ public class TbKafkaSettings {
private short replicationFactor;
@Value("${queue.kafka.max_poll_records:8192}")
- @Getter
private int maxPollRecords;
- @Value("${queue.kafka.max_poll_interval_ms:0}")
- @Getter
+ @Value("${queue.kafka.max_poll_interval_ms:300000}")
private int maxPollIntervalMs;
@Value("${queue.kafka.max_partition_fetch_bytes:16777216}")
- @Getter
private int maxPartitionFetchBytes;
@Value("${queue.kafka.fetch_max_bytes:134217728}")
- @Getter
private int fetchMaxBytes;
@Value("${queue.kafka.use_confluent_cloud:false}")
@@ -93,21 +91,48 @@ public class TbKafkaSettings {
@Setter
private List other;
- public Properties toProps() {
- Properties props = new Properties();
+ public Properties toAdminProps() {
+ Properties props = toProps();
+ props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
+ props.put(AdminClientConfig.RETRIES_CONFIG, retries);
+
+ return props;
+ }
+
+ public Properties toConsumerProps() {
+ Properties props = toProps();
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
+ props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords);
+ props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, maxPartitionFetchBytes);
+ props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes);
+ props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs);
+
+ props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
+ props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
+ return props;
+ }
+
+ public Properties toProducerProps() {
+ Properties props = toProps();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ProducerConfig.RETRIES_CONFIG, retries);
+ props.put(ProducerConfig.ACKS_CONFIG, acks);
+ props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
+ props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
+ props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
+ props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
+ props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
+ return props;
+ }
+
+ private Properties toProps() {
+ Properties props = new Properties();
if (useConfluent) {
props.put("ssl.endpoint.identification.algorithm", sslAlgorithm);
props.put("sasl.mechanism", saslMechanism);
props.put("sasl.jaas.config", saslConfig);
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol);
- } else {
- props.put(ProducerConfig.ACKS_CONFIG, acks);
- props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
- props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs);
- props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
}
if (other != null) {
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java
index 4b68abf929..21291a2d7e 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java
@@ -17,7 +17,7 @@ package org.thingsboard.server.queue.kafka;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@@ -25,7 +25,7 @@ import java.util.HashMap;
import java.util.Map;
@Component
-@ConditionalOnExpression("'${queue.type:null}'=='kafka'")
+@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka")
public class TbKafkaTopicConfigs {
@Value("${queue.kafka.topic-properties.core}")
private String coreProperties;
diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRemoteJsInvokeSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRemoteJsInvokeSettings.java
index cd492e87f1..e34482f985 100644
--- a/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRemoteJsInvokeSettings.java
+++ b/common/queue/src/main/java/org/thingsboard/server/queue/settings/TbQueueRemoteJsInvokeSettings.java
@@ -34,9 +34,6 @@ public class TbQueueRemoteJsInvokeSettings {
@Value("${queue.js.response_poll_interval}")
private int responsePollInterval;
- @Value("${queue.js.response_auto_commit_interval}")
- private int autoCommitInterval;
-
@Value("${queue.js.max_requests_timeout}")
private long maxRequestsTimeout;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
index 8ec43f49a5..458a251f8a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.attributes;
import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -38,4 +39,6 @@ public interface AttributesDao {
ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute);
ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys);
+
+ List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
index a6924960ca..2e18006036 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
@@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -59,6 +60,11 @@ public class BaseAttributesService implements AttributesService {
return attributesDao.findAll(tenantId, entityId, scope);
}
+ @Override
+ public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
+ return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
+ }
+
@Override
public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) {
validate(entityId, scope);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
index f3ef44e6c7..f6c3e195e7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
@@ -46,5 +46,15 @@ public interface AttributeKvRepository extends CrudRepository findAllKeysByDeviceProfileId(@Param("tenantId") UUID tenantId,
+ @Param("deviceProfileId") UUID deviceProfileId);
+
+ @Query(value = "SELECT DISTINCT attribute_key FROM attribute_kv WHERE entity_type = 'DEVICE' " +
+ "AND entity_id in (SELECT id FROM device WHERE tenant_id = :tenantId limit 100) ORDER BY attribute_key", nativeQuery = true)
+ List findAllKeysByTenantId(@Param("tenantId") UUID tenantId);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
index 69616155d2..a95bd4b612 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -135,6 +136,15 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
attributeType))));
}
+ @Override
+ public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
+ if (deviceProfileId != null) {
+ return attributeKvRepository.findAllKeysByDeviceProfileId(tenantId.getId(), deviceProfileId.getId());
+ } else {
+ return attributeKvRepository.findAllKeysByTenantId(tenantId.getId());
+ }
+ }
+
@Override
public ListenableFuture save(TenantId tenantId, EntityId entityId, String attributeType, AttributeKvEntry attribute) {
AttributeKvEntity entity = new AttributeKvEntity();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
index f6e5e8aa40..ff602e6eed 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
@@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
@@ -51,10 +52,15 @@ import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
-import java.util.stream.Collectors;
@Slf4j
@Component
@@ -154,6 +160,15 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
return getFindAllLatestFuture(entityId);
}
+ @Override
+ public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
+ if (deviceProfileId != null) {
+ return tsKvLatestRepository.getKeysByDeviceProfileId(tenantId.getId(), deviceProfileId.getId());
+ } else {
+ return tsKvLatestRepository.getKeysByTenantId(tenantId.getId());
+ }
+ }
+
private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query);
return Futures.transformAsync(future, entryList -> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java
index bb53106254..cd3db69d70 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/TsKvLatestRepository.java
@@ -15,10 +15,25 @@
*/
package org.thingsboard.server.dao.sqlts.latest;
+import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestCompositeKey;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
+import java.util.List;
+import java.util.UUID;
+
public interface TsKvLatestRepository extends CrudRepository {
+ @Query(value = "SELECT DISTINCT ts_kv_dictionary.key AS strKey FROM ts_kv_latest " +
+ "INNER JOIN ts_kv_dictionary ON ts_kv_latest.key = ts_kv_dictionary.key_id " +
+ "WHERE ts_kv_latest.entity_id IN (SELECT id FROM device WHERE device_profile_id = :device_profile_id AND tenant_id = :tenant_id limit 100) ORDER BY ts_kv_dictionary.key", nativeQuery = true)
+ List getKeysByDeviceProfileId(@Param("tenant_id") UUID tenantId, @Param("device_profile_id") UUID deviceProfileId);
+
+ @Query(value = "SELECT DISTINCT ts_kv_dictionary.key AS strKey FROM ts_kv_latest " +
+ "INNER JOIN ts_kv_dictionary ON ts_kv_latest.key = ts_kv_dictionary.key_id " +
+ "WHERE ts_kv_latest.entity_id IN (SELECT id FROM device WHERE tenant_id = :tenant_id limit 100) ORDER BY ts_kv_dictionary.key", nativeQuery = true)
+ List getKeysByTenantId(@Param("tenant_id") UUID tenantId);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
index 3b97e2bb82..7160baf939 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
@@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -40,7 +41,6 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.Validator;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -116,6 +116,11 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesLatestDao.findAllLatest(tenantId, entityId);
}
+ @Override
+ public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
+ return timeseriesLatestDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId);
+ }
+
@Override
public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
validate(entityId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
index 27842dcf18..f086f913c0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.oss.driver.api.core.cql.BoundStatement;
import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
-import com.datastax.oss.driver.api.core.cql.Row;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
import com.google.common.util.concurrent.FutureCallback;
@@ -28,14 +27,13 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
-import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
-import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.TbResultSet;
@@ -43,6 +41,7 @@ import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao;
import org.thingsboard.server.dao.util.NoSqlTsLatestDao;
import javax.annotation.Nullable;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@@ -82,6 +81,11 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
return getFutureAsync(executeAsyncRead(tenantId, stmt), rs -> convertAsyncResultSetToTsKvEntryList(rs));
}
+ @Override
+ public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) {
+ return Collections.emptyList();
+ }
+
@Override
public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
index 7392e0e3d4..0b7156e03e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao.timeseries;
import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
@@ -33,4 +34,5 @@ public interface TimeseriesLatestDao {
ListenableFuture removeLatest(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
+ List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
}
diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql
index 998e268bdc..feeef778ec 100644
--- a/dao/src/main/resources/sql/schema-entities-hsql.sql
+++ b/dao/src/main/resources/sql/schema-entities-hsql.sql
@@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer (
CREATE TABLE IF NOT EXISTS dashboard (
id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
created_time bigint NOT NULL,
- configuration varchar(10000000),
+ configuration varchar,
assigned_customers varchar(1000000),
search_text varchar(255),
tenant_id uuid,
diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql
index 97134a8e19..b610c99803 100644
--- a/dao/src/main/resources/sql/schema-entities-idx.sql
+++ b/dao/src/main/resources/sql/schema-entities-idx.sql
@@ -34,6 +34,8 @@ CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id,
CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type);
+CREATE INDEX IF NOT EXISTS idx_device_device_profile_id ON device(tenant_id, device_profile_id);
+
CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id);
CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 329b486aff..67e0bb4141 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -132,7 +132,7 @@ CREATE TABLE IF NOT EXISTS customer (
CREATE TABLE IF NOT EXISTS dashboard (
id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
created_time bigint NOT NULL,
- configuration varchar(10000000),
+ configuration varchar,
assigned_customers varchar(1000000),
search_text varchar(255),
tenant_id uuid,
diff --git a/docker/docker-compose.hybrid.yml b/docker/docker-compose.hybrid.yml
index 20231292ce..6d8b0b7f9c 100644
--- a/docker/docker-compose.hybrid.yml
+++ b/docker/docker-compose.hybrid.yml
@@ -19,7 +19,7 @@ version: '2.2'
services:
postgres:
restart: always
- image: "postgres:11.6"
+ image: "postgres:12"
ports:
- "5432"
environment:
diff --git a/docker/docker-compose.kafka.yml b/docker/docker-compose.kafka.yml
index c9b235008b..4df63eb8b8 100644
--- a/docker/docker-compose.kafka.yml
+++ b/docker/docker-compose.kafka.yml
@@ -19,7 +19,7 @@ version: '2.2'
services:
kafka:
restart: always
- image: "wurstmeister/kafka:2.12-2.3.0"
+ image: "wurstmeister/kafka:2.13-2.6.0"
ports:
- "9092:9092"
env_file:
diff --git a/docker/docker-compose.postgres.yml b/docker/docker-compose.postgres.yml
index 28fe5a55e2..ee397dd814 100644
--- a/docker/docker-compose.postgres.yml
+++ b/docker/docker-compose.postgres.yml
@@ -19,7 +19,7 @@ version: '2.2'
services:
postgres:
restart: always
- image: "postgres:11.6"
+ image: "postgres:12"
ports:
- "5432"
environment:
diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json
index af75855eaf..8e7ba6d96e 100644
--- a/msa/js-executor/package.json
+++ b/msa/js-executor/package.json
@@ -19,7 +19,7 @@
"azure-sb": "^0.11.1",
"config": "^3.3.1",
"js-yaml": "^3.14.0",
- "kafkajs": "^1.14.0",
+ "kafkajs": "^1.15.0",
"long": "^4.0.0",
"uuid-parse": "^1.1.0",
"uuid-random": "^1.3.2",
diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock
index a2e65d950d..404c059c23 100644
--- a/msa/js-executor/yarn.lock
+++ b/msa/js-executor/yarn.lock
@@ -1665,10 +1665,10 @@ jws@^4.0.0:
jwa "^2.0.0"
safe-buffer "^5.0.1"
-kafkajs@^1.14.0:
- version "1.14.0"
- resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.14.0.tgz#3d998a77bfde54dc502e8e88690eedf0b21a1ed6"
- integrity sha512-W+WCekiooY5rJP3Me5N3gWcQ8O6uG6lw0vv9t+sI+WqXKjKwj2+CWIXJy241x+ITE+1M1D19ABSiL2J8lKja5A==
+kafkajs@^1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-1.15.0.tgz#a5ada0d933edca2149177393562be6fb0875ec3a"
+ integrity sha512-yjPyEnQCkPxAuQLIJnY5dI+xnmmgXmhuOQ1GVxClG5KTOV/rJcW1qA3UfvyEJKTp/RTSqQnUR3HJsKFvHyTpNg==
keyv@^3.0.0:
version "3.1.0"
diff --git a/pom.xml b/pom.xml
index 62c16da7e2..257328b550 100755
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
5.0.2
0.1.14
- 2.3.0
+ 2.6.0
4.1.1
2.57
2.7.7
@@ -729,7 +729,8 @@
docker/haproxy/**
docker/tb-node/**
ui/**
- src/browserslist
+ src/.browserslistrc
+ **/yarn.lock
**/*.raw
**/apache/cassandra/io/**
.run/**
diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml
index 3a1d0e7414..2def6330b3 100644
--- a/transport/coap/src/main/resources/tb-coap-transport.yml
+++ b/transport/coap/src/main/resources/tb-coap-transport.yml
@@ -160,8 +160,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
- # JS response auto commit interval
- response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml
index ef152bb47d..0004400eda 100644
--- a/transport/http/src/main/resources/tb-http-transport.yml
+++ b/transport/http/src/main/resources/tb-http-transport.yml
@@ -153,8 +153,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
- # JS response auto commit interval
- response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
index fc8d970638..210731cde2 100644
--- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
+++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
@@ -182,8 +182,6 @@ queue:
max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
# JS response poll interval
response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
- # JS response auto commit interval
- response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
rule-engine:
topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json
index 3cad30304a..897b7b86ec 100644
--- a/ui-ngx/angular.json
+++ b/ui-ngx/angular.json
@@ -59,6 +59,11 @@
"glob": "marker-shadow.png",
"input": "node_modules/leaflet/dist/images/",
"output": "/"
+ },
+ {
+ "glob": "**/*",
+ "input": "node_modules/material-design-icons/iconfont/",
+ "output": "assets/fonts"
}
],
"styles": [
diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts
index c535878fd2..590bdaa20e 100644
--- a/ui-ngx/src/app/core/http/device-profile.service.ts
+++ b/ui-ngx/src/app/core/http/device-profile.service.ts
@@ -69,4 +69,20 @@ export class DeviceProfileService {
return this.http.get>(url, defaultHttpOptionsFromConfig(config));
}
+ public getDeviceProfileDevicesAttributesKeys(deviceProfileId?: string, config?: RequestConfig): Observable> {
+ let url = `/api/deviceProfile/devices/keys/attributes`;
+ if (isDefinedAndNotNull(deviceProfileId)) {
+ url += `?deviceProfileId=${deviceProfileId}`;
+ }
+ return this.http.get>(url, defaultHttpOptionsFromConfig(config));
+ }
+
+ public getDeviceProfileDevicesTimeseriesKeys(deviceProfileId?: string, config?: RequestConfig): Observable> {
+ let url = `/api/deviceProfile/devices/keys/timeseries`;
+ if (isDefinedAndNotNull(deviceProfileId)) {
+ url += `?deviceProfileId=${deviceProfileId}`;
+ }
+ return this.http.get>(url, defaultHttpOptionsFromConfig(config));
+ }
+
}
diff --git a/ui-ngx/src/app/core/translate/translate-default-compiler.ts b/ui-ngx/src/app/core/translate/translate-default-compiler.ts
index a5bf1bbc17..e6110e593c 100644
--- a/ui-ngx/src/app/core/translate/translate-default-compiler.ts
+++ b/ui-ngx/src/app/core/translate/translate-default-compiler.ts
@@ -65,6 +65,7 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler {
} catch (e) {
console.warn(`Failed to parse source: ${src}`);
console.error(e);
+ return false;
}
const res = tokens.filter(
(value) => typeof value !== 'string' && value.type === 'plural'
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
index 5a9f8858c3..0fe8ed1777 100644
--- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
@@ -40,11 +40,19 @@
filter.key-name
-
-
- {{option}}
+ #keyNameInput
+ (focusin)="onFocus()"
+ [matAutocomplete]="keyName"
+ [matAutocompleteDisabled]="!showAutocomplete">
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
index 202d37be34..b683c5cb17 100644
--- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts
@@ -14,7 +14,7 @@
/// limitations under the License.
///
-import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
+import { Component, ElementRef, Inject, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@@ -32,9 +32,12 @@ import {
} from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
-import { EntityField, entityFields } from '@shared/models/entity.models';
-import { Observable } from 'rxjs';
-import { filter, map, startWith } from 'rxjs/operators';
+import { entityFields } from '@shared/models/entity.models';
+import { Observable, of, Subject } from 'rxjs';
+import { filter, map, mergeMap, publishReplay, refCount, startWith, takeUntil } from 'rxjs/operators';
+import { isDefined } from '@core/utils';
+import { EntityId } from '@shared/models/id/entity-id';
+import { DeviceProfileService } from '@core/http/device-profile.service';
export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo;
@@ -43,6 +46,7 @@ export interface KeyFilterDialogData {
allowUserDynamicSource: boolean;
readonly: boolean;
telemetryKeysOnly: boolean;
+ entityId?: EntityId;
}
@Component({
@@ -53,7 +57,13 @@ export interface KeyFilterDialogData {
})
export class KeyFilterDialogComponent extends
DialogComponent
- implements OnInit, ErrorStateMatcher {
+ implements OnInit, OnDestroy, ErrorStateMatcher {
+
+ @ViewChild('keyNameInput', {static: true}) private keyNameInput: ElementRef;
+
+ private dirty = false;
+ private entityKeysName: Observable>;
+ private destroy$ = new Subject();
keyFilterFormGroup: FormGroup;
@@ -72,19 +82,18 @@ export class KeyFilterDialogComponent extends
submitted = false;
- entityFields: { [fieldName: string]: EntityField };
-
- entityFieldsList: string[];
+ showAutocomplete = false;
- readonly entityField = EntityKeyType.ENTITY_FIELD;
+ filteredKeysName: Observable>;
- filteredEntityFields: Observable;
+ searchText = '';
constructor(protected store: Store,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: KeyFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef,
+ private deviceProfileService: DeviceProfileService,
private dialogs: DialogService,
private translate: TranslateService,
private fb: FormBuilder) {
@@ -104,7 +113,9 @@ export class KeyFilterDialogComponent extends
);
if (!this.data.readonly) {
- this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => {
+ this.keyFilterFormGroup.get('valueType').valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((valueType: EntityKeyValueType) => {
const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
if (prevValue && prevValue !== valueType && predicates && predicates.length) {
@@ -121,11 +132,26 @@ export class KeyFilterDialogComponent extends
}
});
+ this.keyFilterFormGroup.get('key.type').valueChanges.pipe(
+ startWith(this.data.keyFilter.key.type),
+ takeUntil(this.destroy$)
+ ).subscribe((type: EntityKeyType) => {
+ if (type === EntityKeyType.ENTITY_FIELD || isDefined(this.data.entityId)) {
+ this.entityKeysName = null;
+ this.dirty = false;
+ this.showAutocomplete = true;
+ } else {
+ this.showAutocomplete = false;
+ }
+ });
+
this.keyFilterFormGroup.get('key.key').valueChanges.pipe(
- filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName))
+ filter((keyName) =>
+ this.keyFilterFormGroup.get('key.type').value === EntityKeyType.ENTITY_FIELD && entityFields.hasOwnProperty(keyName)),
+ takeUntil(this.destroy$)
).subscribe((keyName: string) => {
const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
- const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING;
+ const newValueType = entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING;
if (prevValueType !== newValueType) {
this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false});
}
@@ -133,18 +159,20 @@ export class KeyFilterDialogComponent extends
} else {
this.keyFilterFormGroup.disable({emitEvent: false});
}
+ }
- this.entityFields = entityFields;
- this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort();
+ ngOnInit() {
+ this.filteredKeysName = this.keyFilterFormGroup.get('key.key').valueChanges
+ .pipe(
+ map(value => value ? value : ''),
+ mergeMap(name => this.fetchEntityName(name))
+ );
}
- ngOnInit(): void {
- this.filteredEntityFields = this.keyFilterFormGroup.get('key.key').valueChanges.pipe(
- startWith(''),
- map(value => {
- return this.entityFieldsList.filter(option => option.startsWith(value));
- })
- );
+ ngOnDestroy() {
+ super.ngOnDestroy();
+ this.destroy$.next();
+ this.destroy$.complete();
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@@ -157,6 +185,21 @@ export class KeyFilterDialogComponent extends
this.dialogRef.close(null);
}
+ clear() {
+ this.keyFilterFormGroup.get('key.key').patchValue('', {emitEvent: true});
+ setTimeout(() => {
+ this.keyNameInput.nativeElement.blur();
+ this.keyNameInput.nativeElement.focus();
+ }, 0);
+ }
+
+ onFocus() {
+ if (!this.dirty && this.showAutocomplete) {
+ this.keyFilterFormGroup.get('key.key').updateValueAndValidity({onlySelf: true, emitEvent: true});
+ this.dirty = true;
+ }
+ }
+
save(): void {
this.submitted = true;
if (this.keyFilterFormGroup.valid) {
@@ -164,4 +207,41 @@ export class KeyFilterDialogComponent extends
this.dialogRef.close(keyFilter);
}
}
+
+ private fetchEntityName(searchText?: string): Observable> {
+ this.searchText = searchText;
+ return this.getEntityKeys().pipe(
+ map(keys => searchText ? keys.filter(key => key.toUpperCase().startsWith(searchText.toUpperCase())) : keys)
+ );
+ }
+
+ private getEntityKeys(): Observable> {
+ if (!this.entityKeysName) {
+ let keyNameObservable: Observable>;
+ switch (this.keyFilterFormGroup.get('key.type').value) {
+ case EntityKeyType.ENTITY_FIELD:
+ keyNameObservable = of(Object.values(entityFields).map(entityField => entityField.keyName).sort());
+ break;
+ case EntityKeyType.ATTRIBUTE:
+ keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesAttributesKeys(
+ this.data.entityId?.id,
+ {ignoreLoading: true}
+ );
+ break;
+ case EntityKeyType.TIME_SERIES:
+ keyNameObservable = this.deviceProfileService.getDeviceProfileDevicesTimeseriesKeys(
+ this.data.entityId?.id,
+ {ignoreLoading: true}
+ );
+ break;
+ default:
+ keyNameObservable = of([]);
+ }
+ this.entityKeysName = keyNameObservable.pipe(
+ publishReplay(1),
+ refCount()
+ );
+ }
+ return this.entityKeysName;
+ }
}
diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
index 2779a35be9..11cb29f149 100644
--- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
+++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
@@ -19,7 +19,8 @@ import {
AbstractControl,
ControlValueAccessor,
FormArray,
- FormBuilder, FormControl,
+ FormBuilder,
+ FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
Validators
@@ -28,12 +29,13 @@ import { Observable, Subscription } from 'rxjs';
import {
EntityKeyType,
entityKeyTypeTranslationMap,
- KeyFilter,
- KeyFilterInfo, keyFilterInfosToKeyFilters
+ KeyFilterInfo,
+ keyFilterInfosToKeyFilters
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils';
import { KeyFilterDialogComponent, KeyFilterDialogData } from '@home/components/filter/key-filter-dialog.component';
+import { EntityId } from '@shared/models/id/entity-id';
@Component({
selector: 'tb-key-filter-list',
@@ -57,6 +59,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
@Input() telemetryKeysOnly = false;
+ @Input() entityId: EntityId;
+
keyFilterListFormGroup: FormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
@@ -170,7 +174,8 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
readonly: this.disabled,
displayUserParameters: this.displayUserParameters,
allowUserDynamicSource: this.allowUserDynamicSource,
- telemetryKeysOnly: this.telemetryKeysOnly
+ telemetryKeysOnly: this.telemetryKeysOnly,
+ entityId: this.entityId
}
}).afterClosed();
}
diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
index c696272d78..2fa920f541 100644
--- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
@@ -96,7 +96,8 @@
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}
+ formControlName="alarms"
+ [deviceProfileId]="null">
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
index 493763756b..b7d13cdbeb 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
@@ -34,6 +34,7 @@
[displayUserParameters]="false"
[allowUserDynamicSource]="false"
[telemetryKeysOnly]="true"
+ [entityId]="entityId"
formControlName="keyFilters">
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
index 4055560b38..0c81ebf397 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.ts
@@ -22,15 +22,16 @@ import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
-import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
-import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
+import { keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
+import { EntityId } from '@shared/models/id/entity-id';
export interface AlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmCondition;
+ entityId?: EntityId;
}
@Component({
@@ -50,6 +51,7 @@ export class AlarmRuleConditionDialogComponent extends DialogComponent,
private fb: FormBuilder,
- private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
index 23e52cf8cd..301175c485 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts
@@ -33,6 +33,7 @@ import {
AlarmRuleConditionDialogData
} from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
import { TimeUnit } from '@shared/models/time/time.models';
+import { EntityId } from '@shared/models/id/entity-id';
@Component({
selector: 'tb-alarm-rule-condition',
@@ -56,6 +57,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
+ @Input()
+ deviceProfileId: EntityId;
+
alarmRuleConditionFormGroup: FormGroup;
specText = '';
@@ -123,7 +127,8 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
- condition: this.disabled ? this.modelValue : deepClone(this.modelValue)
+ condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
+ entityId: this.deviceProfileId
}
}).afterClosed().subscribe((result) => {
if (result) {
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
index c7c77a7f3c..75772663c3 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
@@ -16,7 +16,7 @@
-->
-
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
index 1e7806b455..831f92d0d4 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts
@@ -33,6 +33,7 @@ import {
EditAlarmDetailsDialogComponent,
EditAlarmDetailsDialogData
} from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
+import { EntityId } from '@shared/models/id/entity-id';
@Component({
selector: 'tb-alarm-rule',
@@ -65,6 +66,9 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.requiredValue = coerceBooleanProperty(value);
}
+ @Input()
+ deviceProfileId: EntityId;
+
private modelValue: AlarmRule;
alarmRuleFormGroup: FormGroup;
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
index 5fa0733905..25820aa07e 100644
--- a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
@@ -35,7 +35,7 @@
-
+