Browse Source

Merge branch 'feature/entity-alarm-rules' of github.com:thingsboard/thingsboard into feature/cf-output

pull/14225/head
IrynaMatveieva 11 months ago
parent
commit
d7e194f6d2
  1. 4
      application/src/main/data/json/system/widget_types/attributes_card.json
  2. 2
      application/src/main/data/json/system/widget_types/photo_camera_input.json
  3. 4
      application/src/main/data/json/system/widget_types/timeseries_table.json
  4. 22
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  5. 3
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  6. 14
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  8. 13
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  9. 58
      application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java
  10. 20
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  11. 2
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java
  12. 9
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java
  13. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java
  14. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
  15. 8
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  16. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  17. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  18. 1
      common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java
  19. 4
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java
  20. 12
      common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java
  21. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  22. 36
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java
  23. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  24. 7
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java
  25. 10
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  26. 8
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  27. 11
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  28. 8
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java
  30. 8
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  31. 9
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  32. 11
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  33. 26
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  34. 16
      dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java
  35. 10
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  36. 20
      dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
  37. 30
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  38. 18
      dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java
  39. 15
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java
  40. 14
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java
  41. 13
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java
  42. 14
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java
  43. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  44. 14
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  45. 9
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java
  46. 9
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java
  47. 8
      dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
  48. 36
      dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java
  49. 8
      dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java
  50. 26
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  51. 34
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  52. 13
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  53. 35
      dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java
  54. 38
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  55. 6
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
  56. 9
      dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
  57. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java
  58. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  59. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java
  60. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java
  61. 30
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java
  62. 10
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  63. 11
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java
  64. 1
      dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java
  65. 15
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  66. 12
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  67. 48
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
  68. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java
  69. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java
  70. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
  71. 26
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  72. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
  73. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  74. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  75. 50
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
  76. 2
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java
  77. 111
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
  78. 90
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
  79. 154
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java
  80. 97
      ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch
  81. 2
      ui-ngx/src/app/core/guards/auth.guard.ts
  82. 2
      ui-ngx/src/app/core/http/admin.service.ts
  83. 13
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  84. 38
      ui-ngx/src/app/core/http/git-hub.service.ts
  85. 4
      ui-ngx/src/app/core/services/mobile.service.ts
  86. 10
      ui-ngx/src/app/core/utils.ts
  87. 49
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  88. 60
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts
  89. 164
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  90. 34
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss
  91. 179
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  92. 80
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  93. 278
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  94. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
  95. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
  96. 88
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  97. 188
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  98. 226
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  99. 52
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  100. 37
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss

4
application/src/main/data/json/system/widget_types/attributes_card.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
"controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"<div id='\" + datasourceId +\n \"' class='tbDatasource-container'></div>\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"<div class='tbDatasource-title'>\" +\n tbDatasource.name + \"</div>\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"<table id='\" + tableId +\n \"' class='tbDatasource-table'><col width='30%'><col width='70%'></table>\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"<tr><td id='\" + labelCellId + \"'>\" + dataKey.label +\n \"</td><td id='\" + cellId +\n \"'></td></tr>\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n",
"controllerScript": "self.onInit = function() {\n\n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n for (var i = 0; i < self.ctx.datasources\n .length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"<div id='\" + datasourceId +\n \"' class='tbDatasource-container'></div>\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"<div class='tbDatasource-title'>\" +\n tbDatasource.name + \"</div>\"\n );\n\n var datasourceTitleCell = $(\n '.tbDatasource-title',\n datasourceContainer);\n self.ctx.datasourceTitleCells.push(\n datasourceTitleCell);\n\n var tableId = 'table' + i;\n datasourceContainer.append(\n \"<table id='\" + tableId +\n \"' class='tbDatasource-table'><col width='30%'><col width='70%'></table>\"\n );\n var table = $('#' + tableId, self.ctx\n .$container);\n\n for (var a = 0; a < tbDatasource.dataKeys\n .length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"<tr><td id='\" + labelCellId +\n \"'>\" + dataKey.label +\n \"</td><td id='\" + cellId +\n \"'></td></tr>\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n }\n\n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells\n .length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data\n .length > 0) {\n var tvPair = cellData.data[cellData.data\n .length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals ||\n cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey\n .decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(\n value, decimals, units, true);\n } else {\n txtValue = self.ctx.utilsService\n .customTranslation(value);\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n\n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height / 8;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width / 12;\n }\n datasourceTitleFontSize = Math.min(\n datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells\n .length; i++) {\n self.ctx.datasourceTitleCells[i].css(\n 'font-size', datasourceTitleFontSize +\n 'px');\n }\n var valueFontSize = self.ctx.height / 9;\n var labelFontSize = self.ctx.height / 9;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width / 15;\n labelFontSize = self.ctx.width / 15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size',\n valueFontSize + 'px');\n self.ctx.valueCells[i].css('height',\n valueFontSize * 2.5 + 'px');\n self.ctx.valueCells[i].css('padding', '0px ' +\n valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size',\n labelFontSize + 'px');\n self.ctx.labelCells[i].css('height',\n labelFontSize * 2.5 + 'px');\n self.ctx.labelCells[i].css('padding', '0px ' +\n labelFontSize + 'px');\n }\n}\n\nself.onDestroy = function() {}",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\",\"decimals\":null}"
@ -29,4 +29,4 @@
"public": true
}
]
}
}

2
application/src/main/data/json/system/widget_types/photo_camera_input.json

@ -15,7 +15,7 @@
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-photo-camera-input-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"saveToGallery\":true,\"usePublicGalleryLink\":false,\"imageFormat\":\"image/png\",\"imageQuality\":0.92,\"maxWidth\":640,\"maxHeight\":480},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}"
},
"resources": [
{

4
application/src/main/data/json/system/widget_types/timeseries_table.json

@ -17,7 +17,7 @@
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-timeseries-table-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"tabSortKey\":\"timestamp\"},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}"
},
"resources": [
{
@ -32,4 +32,4 @@
"public": true
}
]
}
}

22
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -27,9 +27,11 @@ import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
@ -74,6 +76,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
@ -320,12 +323,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
EntityId fromId = entityRelation.getFrom();
String relationType = entityRelation.getType();
if (!(CalculatedField.isSupportedRefEntity(toId) || CalculatedField.isSupportedRefEntity(fromId))) {
callback.onSuccess();
return;
}
MultipleTbCallback callbackForToAndFrom = new MultipleTbCallback(2, callback);
processRelationByDirection(EntitySearchDirection.TO, relationType, toId, callbackForToAndFrom, relationAction.apply(fromId));
processRelationByDirection(EntitySearchDirection.FROM, relationType, fromId, callbackForToAndFrom, relationAction.apply(toId));
}
private void processRelationByDirection(EntitySearchDirection direction,
String relationType,
EntityId mainId,
@ -339,9 +346,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
List<CalculatedFieldCtx> matchingCfs = cfsByEntityIdAndProfile.stream()
.filter(cf -> {
var config = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getCalculatedField().getConfiguration();
RelationPathLevel relation = config.getRelation();
return direction.equals(relation.direction()) && relationType.equals(relation.relationType());
if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) {
RelationPathLevel relation = config.getRelation();
return direction.equals(relation.direction()) && relationType.equals(relation.relationType());
} else {
return false;
}
})
.toList();
@ -714,8 +724,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private EntityId getProfileId(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId();
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId();
case ASSET -> Optional.ofNullable(assetProfileCache.get(tenantId, (AssetId) entityId)).map(AssetProfile::getId).orElse(null);
case DEVICE -> Optional.ofNullable(deviceProfileCache.get(tenantId, (DeviceId) entityId)).map(DeviceProfile::getId).orElse(null);
default -> null;
};
}

3
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -266,6 +266,9 @@ public class UserController extends BaseController {
if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) {
throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED);
}
if (user.getAuthority() == Authority.TENANT_ADMIN && userService.countTenantAdmins(user.getTenantId()) == 1) {
throw new ThingsboardException("At least one tenant administrator must remain!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
tbUserService.delete(getTenantId(), getCurrentUser().getCustomerId(), user, getCurrentUser());
}

14
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.AttributeSkipRuleEngineOutputStrategy;
@ -75,6 +76,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION;
@ -204,7 +206,8 @@ public abstract class AbstractCalculatedFieldProcessingService {
}
private ListenableFuture<List<EntityId>> resolveRelatedEntities(TenantId tenantId, EntityId entityId, RelationPathLevel relation) {
ListenableFuture<List<EntityRelation>> relationsFut = relationService.findByRelationPathQueryAsync(tenantId, new EntityRelationPathQuery(entityId, List.of(relation)));
Predicate<EntityRelation> filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo());
ListenableFuture<List<EntityRelation>> relationsFut = relationService.findFilteredRelationsByPathQueryAsync(tenantId, new EntityRelationPathQuery(entityId, List.of(relation)), filter);
return Futures.transform(relationsFut, relations -> {
if (relations == null) {
@ -215,7 +218,11 @@ public abstract class AbstractCalculatedFieldProcessingService {
case FROM -> relations.stream()
.map(EntityRelation::getTo)
.toList();
case TO -> relations.isEmpty() ? List.of() : List.of(relations.get(0).getFrom());
case TO -> relations.stream()
.map(EntityRelation::getFrom)
.findFirst()
.map(List::of)
.orElseGet(Collections::emptyList);
};
}, calculatedFieldCallbackExecutor);
}
@ -237,7 +244,8 @@ public abstract class AbstractCalculatedFieldProcessingService {
case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId)));
case RELATION_PATH_QUERY -> {
var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)),
Predicate<EntityRelation> filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo());
yield Futures.transform(relationService.findFilteredRelationsByPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId), filter),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
}
};

2
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java

@ -162,7 +162,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
}
private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate<CalculatedFieldCtx> filter, Predicate<CalculatedFieldCtx> linkedEntityFilter, Predicate<CalculatedFieldCtx> dynamicSourceFilter, Predicate<CalculatedFieldCtx> relatedEntityFilter) {
if (!CalculatedField.SUPPORTED_REFERENCED_ENTITIES.contains(entityId.getEntityType())) {
if (!CalculatedField.isSupportedRefEntity(entityId)) {
return false;
}

13
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
@ -49,6 +48,7 @@ import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -274,10 +274,13 @@ public class EntityStateSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(RelationActionEvent relationEvent) {
if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) {
tbClusterService.onRelationUpdated(relationEvent.getTenantId(), relationEvent.getRelation(), TbQueueCallback.EMPTY);
} else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) {
tbClusterService.onRelationDeleted(relationEvent.getTenantId(), relationEvent.getRelation(), TbQueueCallback.EMPTY);
EntityRelation relation = relationEvent.getRelation();
if (CalculatedField.isSupportedRefEntity(relation.getFrom()) && CalculatedField.isSupportedRefEntity(relation.getTo())) {
if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) {
tbClusterService.onRelationUpdated(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY);
} else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) {
tbClusterService.onRelationDeleted(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY);
}
}
}

58
application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -299,6 +300,45 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
});
}
@Test
public void testCreateCfAndRelationToRuleChain_checkAggregation() throws Exception {
Asset asset2 = createAsset("Asset 2", assetProfile.getId());
Device device3 = createDevice("Device 3", "1234567890333");
postTelemetry(device3.getId(), "{\"occupied\":true}");
RuleChain ruleChain = new RuleChain();
ruleChain.setName("RuleChain");
ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
postTelemetry(ruleChain.getId(), "{\"occupied\":true}");
createEntityRelation(asset2.getId(), device3.getId(), "Contains");
createEntityRelation(asset2.getId(), ruleChain.getId(), "Contains");
createOccupancyCF(asset2.getId());
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
verifyTelemetry(asset2.getId(), Map.of(
"freeSpaces", "0",
"occupiedSpaces", "1",
"totalSpaces", "1"
));
});
postTelemetry(ruleChain.getId(), "{\"occupied\":true}");
await().alias("update telemetry on rule chain and no aggregation performed").atMost(deduplicationInterval, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
verifyTelemetry(asset2.getId(), Map.of(
"freeSpaces", "0",
"occupiedSpaces", "1",
"totalSpaces", "1"
));
});
}
@Test
public void testDeleteCf_checkNoAggregation() throws Exception {
CalculatedField cf = createOccupancyCF(asset.getId());
@ -466,6 +506,24 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
});
}
@Test
public void testDeleteEntityByRelation_checkAggregation() throws Exception {
createOccupancyCF(asset.getId());
checkInitialCalculation();
doDelete("/api/device/" + device1.getId()).andExpect(status().isOk());
await().alias("create relation and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of(
"freeSpaces", "1",
"occupiedSpaces", "0",
"totalSpaces", "1"
));
});
}
@Test
public void testUpdateRelationPath_checkAggregation() throws Exception {
CalculatedField cf = createOccupancyCF(asset.getId());

20
application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

@ -284,6 +284,26 @@ public class UserControllerTest extends AbstractControllerTest {
ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testShouldNotDeleteLastTenantAdmin() throws Exception {
loginSysAdmin();
User tenantAdmin2 = new User();
tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin2.setTenantId(tenantId);
tenantAdmin2.setEmail("tenant2@thingsboard.io");
tenantAdmin2 = doPost("/api/user", tenantAdmin2, User.class);
// delete second tenant admin - ok
doDelete("/api/user/" + tenantAdmin2.getId().getId().toString())
.andExpect(status().isOk());
// delete last tenant admin - forbidden
doDelete("/api/user/" + tenantAdminUser.getId().getId().toString())
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("At least one tenant administrator must remain!")));
}
@Test
public void testSaveUserWithInvalidEmail() throws Exception {
loginSysAdmin();

2
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java

@ -98,7 +98,7 @@ public class JobManagerTest extends AbstractControllerTest {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
Job job = findJobById(jobId);
assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING);
assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1);
assertThat(job.getResult().getSuccessfulCount()).isBetween(0, tasksCount - 1);
assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount);
});
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {

9
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java

@ -27,17 +27,16 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest {
/*
* Some tests are overridden because they are based on
* tenant partitioning strategy (subsequent tasks processing within a tenant)
* */
* Some tests are overridden because they are based on
* tenant partitioning strategy (subsequent tasks processing within a tenant)
* */
@Override
public void testCancelJob_simulateTaskProcessorRestart() throws Exception {
public void testCancelJob_simulateTaskProcessorRestart() {
}
@Override
public void testSubmitJob_generalError() {
}
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -26,6 +27,8 @@ public interface EntityDaoService {
Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId);
FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId);
default long countByTenantId(TenantId tenantId) {
throw new IllegalArgumentException("Not implemented for " + getEntityType());
}

4
common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -38,6 +39,8 @@ public interface EntityService {
Optional<CustomerId> fetchEntityCustomerId(TenantId tenantId, EntityId entityId);
FluentFuture<Optional<CustomerId>> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId);
Optional<HasId<?>> fetchEntity(TenantId tenantId, EntityId entityId);
Map<EntityId, EntityInfo> fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set<EntityId> entityIds);
@ -47,4 +50,5 @@ public interface EntityService {
long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query);
PageData<EntityData> findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query);
}

8
common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -32,9 +32,6 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
/**
* Created by Victor Basanets on 8/27/2017.
*/
public interface EntityViewService extends EntityDaoService {
EntityView saveEntityView(EntityView entityView);
@ -55,6 +52,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache);
ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId);
EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name);
ListenableFuture<EntityView> findEntityViewByTenantIdAndNameAsync(TenantId tenantId, String name);
@ -77,8 +76,6 @@ public interface EntityViewService extends EntityDaoService {
ListenableFuture<List<EntityView>> findEntityViewsByQuery(TenantId tenantId, EntityViewSearchQuery query);
ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId);
ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId);
List<EntityView> findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
@ -98,4 +95,5 @@ public interface EntityViewService extends EntityDaoService {
PageData<EntityView> findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink);
PageData<EntityView> findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink);
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChainType;
import java.util.List;
import java.util.function.Predicate;
/**
* Created by ashvayka on 27.04.17.
@ -86,6 +87,8 @@ public interface RelationService {
ListenableFuture<List<EntityRelation>> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
ListenableFuture<List<EntityRelation>> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate<EntityRelation> relationFilter);
List<EntityRelation> findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
// TODO: This method may be useful for some validations in the future

3
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -111,8 +111,9 @@ public interface UserService extends EntityDaoService {
void removeMobileSession(TenantId tenantId, String mobileToken);
int countTenantAdmins(TenantId tenantId);
PageData<User> findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink);
boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java

@ -20,4 +20,5 @@ import org.thingsboard.server.common.data.id.CustomerId;
public interface HasCustomerId {
CustomerId getCustomerId();
}

4
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java

@ -65,6 +65,10 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT
));
public static boolean isSupportedRefEntity(EntityId entity) {
return SUPPORTED_REFERENCED_ENTITIES.contains(entity.getEntityType());
}
private TenantId tenantId;
private EntityId entityId;

12
common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java

@ -55,11 +55,17 @@ public abstract class JobResult implements Serializable {
public void processTaskResult(TaskResult taskResult) {
if (taskResult.isSuccess()) {
successfulCount++;
if (totalCount == null || successfulCount < totalCount) {
successfulCount++;
}
} else if (taskResult.isDiscarded()) {
discardedCount++;
if (totalCount == null || discardedCount < totalCount) {
discardedCount++;
}
} else {
failedCount++;
if (totalCount == null || failedCount < totalCount) {
failedCount++;
}
if (results.size() < 100) { // preserving only first 100 errors, not reprocessing if there are more failures
results.add(taskResult);
}

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
@ -196,14 +195,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
root = new CoapResource("");
coapServer.add(root);
}
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE, serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE)));
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE,serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE)));
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE));
root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE));
}
return leshanServer;
}
private void setServerWithCredentials(LeshanServerBuilder builder) {
// private void setServerWithCredentials(LeshanServerBuilder builder) {
if (this.config.getSslCredentials() != null) {
SslCredentials sslCredentials = this.config.getSslCredentials();
builder.setPublicKey(sslCredentials.getPublicKey());

36
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java

@ -34,21 +34,15 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FIRMWARE_UPDATE_COAP_RESOURCE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SOFTWARE_UPDATE_COAP_RESOURCE;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.calculateSzx;
@Slf4j
public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
private final ConcurrentMap<String, ObserveRelation> tokenToObserveRelationMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, AtomicInteger> tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>();
private final OtaPackageDataCache otaPackageDataCache;
private final int chunkSize;
private final int maxResourceBodySize;
public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name, int chunkSize, int maxResourceBodySize) {
public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name) {
super(name);
this.otaPackageDataCache = otaPackageDataCache;
this.chunkSize = chunkSize;
this.maxResourceBodySize = maxResourceBodySize;
this.setObservable(true); // enable observing
this.addObserver(new CoapResourceObserver());
}
@ -141,29 +135,23 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 1
);
UUID currentId = UUID.fromString(idStr);
log.info("Start Read ota data (path): [{}]", exchange.getRequestOptions().getUriPath().toString());
Response response = new Response(CoAP.ResponseCode.CONTENT);
byte[] otaData = this.getOtaData(currentId);
if (otaData != null && otaData.length > 0) {
if (otaData.length <= this.maxResourceBodySize) {
log.info("Read ota data (length): [{}]", otaData.length);
response.setPayload(otaData);
int chunkSize = calculateSzx(this.chunkSize);
if (exchange.getRequestOptions().hasBlock2()) {
chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
} else if (exchange.getRequestOptions().hasBlock1()) {
chunkSize = exchange.getRequestOptions().getBlock1().getSzx();
}
log.info("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), otaData.length, chunkSize, false);
boolean lastFlag = otaData.length <= this.chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
response.setType(CoAP.Type.CON);
exchange.respond(response);
log.debug("Read ota data (length): [{}]", otaData.length);
response.setPayload(otaData);
if (exchange.getRequestOptions().getBlock2() != null) {
int szx = exchange.getRequestOptions().getBlock2().getSzx();
int chunkSize = exchange.getRequestOptions().getBlock2().getSize();
boolean lastFlag = otaData.length <= chunkSize;
response.getOptions().setBlock2(szx, lastFlag, 0);
log.trace("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], szx [{}], moreFlag [{}]", currentId, otaData.length, chunkSize, szx, lastFlag);
} else {
log.info("Ota package size: [{}] is larger than server's MAX_RESOURCE_BODY_SIZE [{}]", otaData.length, this.maxResourceBodySize);
log.trace("With block1 Send currentId: [{}], length: [{}], ", currentId, otaData.length);
}
exchange.respond(response);
} else {
log.info("Ota packaged currentId: [{}] is not found.", currentId.toString());
log.trace("Ota packaged currentId: [{}] is not found.", currentId);
}
}

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -121,10 +121,6 @@ public class LwM2MTransportUtil {
}
}
public static List<LwM2MBootstrapServerCredential> getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) {
return toLwM2MClientProfile(deviceProfile).getBootstrap();
}
public static String fromVersionedIdToObjectId(String pathIdVer) {
try {
if (pathIdVer == null) {
@ -399,13 +395,6 @@ public class LwM2MTransportUtil {
}
}
public static int calculateSzx(int size) {
if (size < 16 || size > 1024 || (size & (size - 1)) != 0) {
throw new IllegalArgumentException("Size must be a power of 2 between 16 and 1024.");
}
return (int) (Math.log(size / 16) / Math.log(2));
}
public static ConcurrentHashMap<Integer, String[]> groupByObjectIdVersionedIds(Set<String> targetIds) {
return targetIds.stream()
.collect(Collectors.groupingBy(

7
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java

@ -39,6 +39,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@ -135,6 +136,12 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
.map(model -> model); // necessary to cast to HasId<?>
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return findAiModelByTenantIdAndIdAsync(tenantId, new AiModelId(entityId.getId()))
.transform(modelOpt -> modelOpt.map(model -> model), directExecutor()); // necessary to cast to HasId<?>
}
@Override
public long countByTenantId(TenantId tenantId) {
return aiModelDao.countByTenantId(tenantId);

10
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
@ -81,6 +80,7 @@ import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -97,8 +97,8 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
private final AlarmDao alarmDao;
private final EntityService entityService;
@TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(AlarmTypesCacheEvictEvent event) {
TenantId tenantId = event.getTenantId();
cache.evict(tenantId);
@ -460,6 +460,12 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
return Optional.ofNullable(findAlarmById(tenantId, new AlarmId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findAlarmByIdAsync(tenantId, new AlarmId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ALARM;

8
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
@ -49,6 +50,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("AssetProfileDaoService")
@ -323,6 +325,12 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService<AssetP
return Optional.ofNullable(findAssetProfileById(tenantId, new AssetProfileId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(assetProfileDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ASSET_PROFILE;

11
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -64,6 +64,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -95,8 +96,8 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Autowired
private JpaExecutorService executor;
@TransactionalEventListener(classes = AssetCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(AssetCacheEvictEvent event) {
List<AssetCacheKey> keys = new ArrayList<>(2);
keys.add(new AssetCacheKey(event.getTenantId(), event.getNewName()));
@ -531,6 +532,12 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return Optional.ofNullable(findAssetById(tenantId, new AssetId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findAssetByIdAsync(tenantId, new AssetId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return assetDao.countByTenantId(tenantId);

8
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -42,6 +43,7 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -245,6 +247,12 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return Optional.ofNullable(findById(tenantId, new CalculatedFieldId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(calculatedFieldDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;

2
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java

@ -47,6 +47,6 @@ public interface CalculatedFieldDao extends Dao<CalculatedField> {
List<CalculatedField> removeAllByEntityId(TenantId tenantId, EntityId entityId);
long countCFByEntityId(TenantId tenantId, EntityId entityId);
long countByEntityIdAndTypeNot(TenantId tenantId, EntityId entityId, CalculatedFieldType type);
}

8
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.customer;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -57,6 +58,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("CustomerDaoService")
@ -302,6 +304,12 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
return Optional.ofNullable(findCustomerById(tenantId, new CustomerId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findCustomerByIdAsync(tenantId, new CustomerId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return customerDao.countByTenantId(tenantId);

9
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.dashboard;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -61,6 +62,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("DashboardDaoService")
@ -70,6 +72,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
public static final String INCORRECT_DASHBOARD_ID = "Incorrect dashboardId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@Autowired
private DashboardDao dashboardDao;
@ -424,6 +427,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return Optional.ofNullable(findDashboardById(tenantId, new DashboardId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findDashboardByIdAsync(tenantId, new DashboardId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return dashboardDao.countByTenantId(tenantId);

11
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -64,6 +65,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString;
@ -369,6 +371,12 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
return Optional.ofNullable(findDeviceProfileById(tenantId, new DeviceProfileId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(deviceProfileDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.DEVICE_PROFILE;
@ -432,8 +440,7 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
if (certificates.length > 1) {
return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue);
}
} catch (CertificateException ignored) {
}
} catch (CertificateException ignored) {}
return EncryptionUtil.certTrimNewLines(certificateValue);
}

26
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.device;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -91,6 +92,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -128,23 +130,21 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
validateId(deviceId, id -> INCORRECT_DEVICE_ID + id);
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return cache.get(new DeviceCacheKey(deviceId),
() -> deviceDao.findById(tenantId, deviceId.getId()));
} else {
return cache.get(new DeviceCacheKey(tenantId, deviceId),
() -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()));
}
return findDeviceByIdInternal(tenantId, deviceId);
}
@Override
public ListenableFuture<Device> findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceByIdAsync [{}]", deviceId);
validateId(deviceId, id -> INCORRECT_DEVICE_ID + id);
return executor.submit(() -> findDeviceByIdInternal(tenantId, deviceId));
}
private Device findDeviceByIdInternal(TenantId tenantId, DeviceId deviceId) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
return deviceDao.findByIdAsync(tenantId, deviceId.getId());
return cache.get(new DeviceCacheKey(deviceId), () -> deviceDao.findById(tenantId, deviceId.getId()));
} else {
return deviceDao.findDeviceByTenantIdAndIdAsync(tenantId, deviceId.getId());
return cache.get(new DeviceCacheKey(tenantId, deviceId), () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()));
}
}
@ -275,8 +275,8 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
}
}
@TransactionalEventListener(classes = DeviceCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(DeviceCacheEvictEvent event) {
List<DeviceCacheKey> toEvict = new ArrayList<>(3);
toEvict.add(new DeviceCacheKey(event.getTenantId(), event.getNewName()));
@ -748,6 +748,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
return Optional.ofNullable(findDeviceById(tenantId, new DeviceId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findDeviceByIdAsync(tenantId, new DeviceId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.DEVICE;

16
dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.domain;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -39,17 +40,16 @@ import org.thingsboard.server.dao.service.validator.DomainDataValidator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
@Service
public class DomainServiceImpl extends AbstractEntityService implements DomainService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@Autowired
private OAuth2ClientDao oauth2ClientDao;
@Autowired
@ -66,8 +66,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build());
return savedDomain;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("domain_name_key", "Domain with such name and scheme already exists!"));
checkConstraintViolation(e, "domain_name_key", "Domain with such name and scheme already exists!");
throw e;
}
}
@ -142,6 +141,12 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
return Optional.ofNullable(findDomainById(tenantId, new DomainId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(domainDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
@ -163,4 +168,5 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
public EntityType getEntityType() {
return EntityType.DOMAIN;
}
}

10
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.edge;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -83,6 +84,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -137,8 +139,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Value("${edges.state.persistToTelemetry:false}")
private boolean persistToTelemetry;
@TransactionalEventListener(classes = EdgeCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(EdgeCacheEvictEvent event) {
List<EdgeCacheKey> keys = new ArrayList<>(2);
keys.add(new EdgeCacheKey(event.getTenantId(), event.getNewName()));
@ -629,6 +631,12 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
return Optional.ofNullable(findEdgeById(tenantId, new EdgeId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.EDGE;

20
dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.entity;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@ -67,15 +68,13 @@ import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID;
import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_NAME;
import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_TYPE;
import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink;
import static org.thingsboard.server.dao.service.Validator.validateId;
/**
* Created by ashvayka on 04.05.17.
*/
@Service
@Slf4j
public class BaseEntityService extends AbstractEntityService implements EntityService {
@ -93,7 +92,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
@Autowired
@Lazy
EntityServiceRegistry entityServiceRegistry;
private EntityServiceRegistry entityServiceRegistry;
@Autowired
private EdqsService edqsService;
@ -198,6 +197,11 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
return fetchAndConvert(tenantId, entityId, this::getCustomerId);
}
@Override
public FluentFuture<Optional<CustomerId>> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId) {
return fetchAndConvertAsync(tenantId, entityId, this::getCustomerId);
}
@Override
public Optional<NameLabelAndCustomerDetails> fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId) {
log.trace("Executing fetchNameLabelAndCustomerDetails [{}]", entityId);
@ -239,6 +243,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
return entityOpt.map(converter);
}
private <T> FluentFuture<Optional<T>> fetchAndConvertAsync(TenantId tenantId, EntityId entityId, Function<HasId<?>, T> converter) {
EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType());
return entityDaoService.findEntityAsync(tenantId, entityId)
.transform(entityOpt -> entityOpt.map(converter), directExecutor());
}
private String getName(HasId<?> entity) {
return entity instanceof HasName ? ((HasName) entity).getName() : null;
}
@ -329,7 +339,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe
}
if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) &&
(query.getLatestValues() == null || query.getLatestValues().isEmpty())) {
(query.getLatestValues() == null || query.getLatestValues().isEmpty())) {
return false;
}

30
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.entityview;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -62,13 +63,11 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validateString;
/**
* Created by Victor Basanets on 8/28/2017.
*/
@Service("EntityViewDaoService")
@Slf4j
public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityViewCacheKey, EntityViewCacheValue, EntityViewEvictEvent> implements EntityViewService {
@ -188,6 +187,17 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
public EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache) {
log.trace("Executing findEntityViewById [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return findEntityViewByIdInternal(tenantId, entityViewId, putInCache);
}
@Override
public ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) {
log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return service.submit(() -> findEntityViewByIdInternal(tenantId, entityViewId, true));
}
private EntityView findEntityViewByIdInternal(TenantId tenantId, EntityViewId entityViewId, boolean putInCache) {
EntityViewCacheValue value = cache.get(EntityViewCacheKey.byId(entityViewId), () -> {
EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId());
return new EntityViewCacheValue(entityView, null);
@ -202,7 +212,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return cache.getAndPutInTransaction(EntityViewCacheKey.byName(tenantId, name),
() -> entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null)
, EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true);
}
@Override
@ -319,13 +328,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return entityViews;
}
@Override
public ListenableFuture<EntityView> findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) {
log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId);
validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id);
return entityViewDao.findByIdAsync(tenantId, entityViewId.getId());
}
@Override
public ListenableFuture<List<EntityView>> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) {
log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId);
@ -491,6 +493,12 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return Optional.ofNullable(findEntityViewById(tenantId, new EntityViewId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findEntityViewByIdAsync(tenantId, new EntityViewId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ENTITY_VIEW;

18
dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.job;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -44,6 +45,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.job.JobStatus.CANCELLED;
import static org.thingsboard.server.common.data.job.JobStatus.COMPLETED;
import static org.thingsboard.server.common.data.job.JobStatus.FAILED;
@ -87,11 +89,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
}
job.getResult().setCancellationTs(System.currentTimeMillis());
JobStatus prevStatus = job.getStatus();
if (job.getStatus() == QUEUED) {
job.setStatus(CANCELLED); // setting cancelled status right away, because we don't expect stats for cancelled tasks
} else if (job.getStatus() == PENDING) {
job.setStatus(RUNNING);
}
job.setStatus(CANCELLED);
saveJob(tenantId, job, true, prevStatus);
}
@ -145,7 +143,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
}
}
if (job.getStatus() == RUNNING) {
if (job.getStatus().isOneOf(RUNNING, CANCELLED)) {
if (result.getTotalCount() != null && result.getCompletedCount() >= result.getTotalCount()) {
if (result.getCancellationTs() > 0) {
job.setStatus(CANCELLED);
@ -193,7 +191,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
private void checkWaitingJobs(TenantId tenantId, JobType jobType) {
Job queuedJob = jobDao.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId, jobType, QUEUED);
if (queuedJob == null) {
if (queuedJob == null || jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, jobType, PENDING, RUNNING)) {
return;
}
queuedJob.setStatus(PENDING);
@ -245,6 +243,12 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
return Optional.ofNullable(findJobById(tenantId, (JobId) entityId));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(jobDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
jobDao.removeById(tenantId, id.getId());

15
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.mobile;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -40,11 +41,11 @@ import org.thingsboard.server.dao.service.DataValidator;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.checkNotNull;
@Slf4j
@ -60,7 +61,6 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
@Autowired
private DataValidator<MobileAppBundle> mobileAppBundleDataValidator;
@Override
public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) {
log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle);
@ -71,8 +71,8 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
return savedMobileApp;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!",
"mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!"));
"mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!",
"mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!");
throw e;
}
}
@ -149,6 +149,12 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
return Optional.ofNullable(findMobileAppBundleById(tenantId, new MobileAppBundleId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(mobileAppBundleDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
@ -167,4 +173,5 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements
.collect(Collectors.toList());
mobileAppBundleInfo.setOauth2ClientInfos(clients);
}
}

14
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.mobile;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -35,9 +36,10 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.Validator;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
@Service
public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService {
@ -58,8 +60,7 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build());
return savedMobileApp;
} catch (Exception e) {
checkConstraintViolation(e,
Map.of("mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!"));
checkConstraintViolation(e, "mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!");
throw e;
}
}
@ -88,6 +89,12 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
return Optional.ofNullable(findMobileAppById(tenantId, new MobileAppId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(mobileAppDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
@ -117,4 +124,5 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
public EntityType getEntityType() {
return EntityType.MOBILE_APP;
}
}

13
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
@ -38,6 +39,8 @@ import org.thingsboard.server.dao.service.DataValidator;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
@RequiredArgsConstructor
@ -129,13 +132,17 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
return Optional.ofNullable(findNotificationRequestById(tenantId, new NotificationRequestId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationRequestDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_REQUEST;
}
private static class NotificationRequestValidator extends DataValidator<NotificationRequest> {
}
private static class NotificationRequestValidator extends DataValidator<NotificationRequest> {}
}

14
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -33,9 +34,10 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@RequiredArgsConstructor
public class DefaultNotificationRuleService extends AbstractEntityService implements NotificationRuleService, EntityDaoService {
@ -56,9 +58,7 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem
.created(notificationRule.getId() == null).build());
return savedRule;
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"uq_notification_rule_name", "Notification rule with such name already exists"
));
checkConstraintViolation(e, "uq_notification_rule_name", "Notification rule with such name already exists");
throw e;
}
}
@ -114,6 +114,12 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem
return Optional.ofNullable(findNotificationRuleById(tenantId, new NotificationRuleId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationRuleDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE;

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -37,6 +38,8 @@ import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import java.util.Optional;
import java.util.Set;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
@RequiredArgsConstructor
@ -95,6 +98,12 @@ public class DefaultNotificationService implements NotificationService, EntityDa
return Optional.ofNullable(findNotificationById(tenantId, new NotificationId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION;

14
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -43,10 +44,11 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.user.UserService;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
@RequiredArgsConstructor
@ -65,9 +67,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
.created(notificationTarget.getId() == null).build());
return savedTarget;
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"uq_notification_target_name", "Recipients group with such name already exists"
));
checkConstraintViolation(e, "uq_notification_target_name", "Recipients group with such name already exists");
throw e;
}
}
@ -180,6 +180,12 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
return Optional.ofNullable(findNotificationTargetById(tenantId, new NotificationTargetId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationTargetDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET;

9
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.notification;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -37,6 +38,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@RequiredArgsConstructor
public class DefaultNotificationTemplateService extends AbstractEntityService implements NotificationTemplateService, EntityDaoService {
@ -144,6 +147,12 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im
return Optional.ofNullable(findNotificationTemplateById(tenantId, new NotificationTemplateId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(notificationTemplateDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE;

9
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.oauth2;
import com.google.common.util.concurrent.FluentFuture;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -41,6 +42,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Slf4j
@Service("OAuth2ClientService")
@ -107,7 +109,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
.tenantId(tenantId)
.entityId(oAuth2ClientId)
.build());
}
@Override
@ -149,6 +150,12 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
return Optional.ofNullable(findOAuth2ClientById(tenantId, new OAuth2ClientId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(oauth2ClientDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
@Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {

8
dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.ota;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -47,6 +48,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import java.nio.ByteBuffer;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -254,6 +256,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
return Optional.ofNullable(findOtaPackageInfoById(tenantId, new OtaPackageId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findOtaPackageInfoByIdAsync(tenantId, new OtaPackageId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;

36
dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.queue;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
@ -42,6 +43,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("QueueDaoService")
@Slf4j
@RequiredArgsConstructor
@ -127,7 +131,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ
@Override
public void deleteQueuesByTenantId(TenantId tenantId) {
Validator.validateId(tenantId, "Incorrect tenant id for delete queues request.");
validateId(tenantId, __ -> "Incorrect tenant id for delete queues request.");
tenantQueuesRemover.removeEntities(tenantId, tenantId);
}
@ -141,24 +145,30 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ
return Optional.ofNullable(findQueueById(tenantId, new QueueId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(queueDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.QUEUE;
}
private PaginatedRemover<TenantId, Queue> tenantQueuesRemover =
new PaginatedRemover<>() {
private final PaginatedRemover<TenantId, Queue> tenantQueuesRemover = new PaginatedRemover<>() {
@Override
protected PageData<Queue> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return queueDao.findQueuesByTenantId(id, pageLink);
}
@Override
protected PageData<Queue> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return queueDao.findQueuesByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Queue entity) {
deleteQueue(tenantId, entity.getId());
}
};
@Override
protected void removeEntity(TenantId tenantId, Queue entity) {
deleteQueue(tenantId, entity.getId());
}
};
private TenantId getSystemOrIsolatedTenantId(TenantId tenantId) {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
@ -167,7 +177,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ
return tenantId;
}
}
return TenantId.SYS_TENANT_ID;
}
}

8
dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.queue;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -35,6 +36,7 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -106,6 +108,12 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu
return Optional.ofNullable(findQueueStatsById(tenantId, new QueueStatsId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(queueStatsDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.QUEUE_STATS;

26
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -71,6 +71,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber;
@ -507,6 +508,11 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<List<EntityRelation>> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) {
return findFilteredRelationsByPathQueryAsync(tenantId, relationPathQuery, null);
}
@Override
public ListenableFuture<List<EntityRelation>> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate<EntityRelation> relationFilter) {
log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery);
validateId(tenantId, id -> "Invalid tenant id: " + id);
validate(relationPathQuery);
@ -518,10 +524,24 @@ public class BaseRelationService implements RelationService {
case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
};
return Futures.transform(relationsFuture, entityRelations -> entityRelations.size() > limit ?
entityRelations.subList(0, limit) : entityRelations, MoreExecutors.directExecutor());
return Futures.transform(relationsFuture, entityRelations -> {
if (entityRelations == null || entityRelations.isEmpty()) {
return Collections.emptyList();
}
List<EntityRelation> relations = relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations;
return relations.size() > limit ? relations.subList(0, limit) : relations;
}, MoreExecutors.directExecutor());
}
return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit));
return executor.submit(() -> {
List<EntityRelation> entityRelations = relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit);
return relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations;
});
}
private List<EntityRelation> filterRelations(List<EntityRelation> entityRelations, Predicate<EntityRelation> relationFilter) {
return entityRelations.stream()
.filter(relationFilter)
.toList();
}
@Override

34
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -17,11 +17,11 @@ package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;
@ -356,8 +356,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
imageName = imageName + type + " image";
UpdateResult result = convertToImageUrl(entity.getTenantId(), imageName, entity.getImage(), Collections.emptyMap());
entity.setImage(result.getValue());
return result.isUpdated();
entity.setImage(result.value());
return result.updated();
}
@Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@ -373,8 +373,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
Map<String, String> imagesLinks = getResourcesLinks(widgetTypeDetails.getResources());
UpdateResult result = convertToImageUrl(tenantId, prefix + " image", widgetTypeDetails.getImage(), imagesLinks);
boolean updated = result.isUpdated();
widgetTypeDetails.setImage(result.getValue());
boolean updated = result.updated();
widgetTypeDetails.setImage(result.value());
if (widgetTypeDetails.getDescriptor().isObject()) {
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
@ -397,8 +397,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
Map<String, String> imagesLinks = getResourcesLinks(dashboard.getResources());
var result = convertToImageUrl(tenantId, prefix + " image", dashboard.getImage(), imagesLinks);
boolean updated = result.isUpdated();
dashboard.setImage(result.getValue());
boolean updated = result.updated();
dashboard.setImage(result.value());
updated |= convertToImageUrlsByMapping(tenantId, DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), dashboard.getConfiguration(), imagesLinks);
updated |= convertToImageUrls(tenantId, prefix, dashboard.getConfiguration(), imagesLinks);
@ -409,10 +409,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAllByMapping(configuration, mapping, templateParams, (name, value) -> {
UpdateResult result = convertToImageUrl(tenantId, name, value, links);
if (result.isUpdated()) {
if (result.updated()) {
updated.set(true);
}
return result.getValue();
return result.value();
});
return updated.get();
}
@ -514,10 +514,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAll(root, title, (path, value) -> {
UpdateResult result = convertToImageUrl(tenantId, path, value, true, links);
if (result.isUpdated()) {
if (result.updated()) {
updated.set(true);
}
return result.getValue();
return result.value();
});
return updated.get();
}
@ -678,16 +678,18 @@ public class BaseImageService extends BaseResourceService implements ImageServic
private String getImageLink(String value) {
if (value.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images")) {
return StringUtils.removeStart(value, DataConstants.TB_IMAGE_PREFIX);
return Strings.CS.removeStart(value, DataConstants.TB_IMAGE_PREFIX);
} else {
return null;
}
}
@Data(staticConstructor = "of")
private static class UpdateResult {
private final boolean updated;
private final String value;
private record UpdateResult(boolean updated, String value) {
static UpdateResult of(boolean updated, String value) {
return new UpdateResult(updated, value);
}
}
}

13
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@ -79,6 +80,7 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -107,7 +109,8 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao);
}
@Autowired @Lazy
@Autowired
@Lazy
private ImageService imageService;
private static final Map<String, String> DASHBOARD_RESOURCES_MAPPING = Map.of(
@ -275,7 +278,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
public TbResource toResource(TenantId tenantId, ResourceExportData exportData) {
if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
throw new IllegalArgumentException("Image import not supported");
}
@ -457,6 +460,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return Optional.ofNullable(findResourceInfoById(tenantId, new TbResourceId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findResourceInfoByIdAsync(tenantId, new TbResourceId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;

35
dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.rpc;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -33,6 +34,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -40,6 +42,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Slf4j
@RequiredArgsConstructor
public class BaseRpcService implements RpcService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RPC_ID = "Incorrect rpcId ";
@ -113,21 +116,29 @@ public class BaseRpcService implements RpcService {
return Optional.ofNullable(findById(tenantId, new RpcId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findRpcByIdAsync(tenantId, new RpcId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.RPC;
}
private PaginatedRemover<TenantId, Rpc> tenantRpcRemover =
new PaginatedRemover<>() {
@Override
protected PageData<Rpc> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return rpcDao.findAllRpcByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Rpc entity) {
deleteRpc(tenantId, entity.getId());
}
};
private final PaginatedRemover<TenantId, Rpc> tenantRpcRemover = new PaginatedRemover<>() {
@Override
protected PageData<Rpc> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return rpcDao.findAllRpcByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Rpc entity) {
deleteRpc(tenantId, entity.getId());
}
};
}

38
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.rule;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
@ -28,6 +29,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
@ -80,6 +83,7 @@ import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.common.data.DataConstants.TENANT;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@ -87,9 +91,6 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber;
import static org.thingsboard.server.dao.service.Validator.validateString;
/**
* Created by igor on 3/12/18.
*/
@Service("RuleChainDaoService")
@Slf4j
public class BaseRuleChainService extends AbstractEntityService implements RuleChainService {
@ -98,6 +99,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String TB_RULE_CHAIN_INPUT_NODE = "org.thingsboard.rule.engine.flow.TbRuleChainInputNode";
@Autowired
private RuleChainDao ruleChainDao;
@ -260,7 +262,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId();
}
if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId))
|| (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
|| (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) {
ruleChain.setFirstRuleNodeId(firstRuleNodeId);
}
if (ruleChainMetaData.getConnections() != null) {
@ -876,6 +878,17 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
return Optional.ofNullable(hasId);
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
ListenableFuture<? extends BaseDataWithAdditionalInfo<? extends UUIDBased>> future;
if (entityId.getEntityType() == EntityType.RULE_NODE) {
future = findRuleNodeByIdAsync(tenantId, new RuleNodeId(entityId.getId()));
} else {
future = findRuleChainByIdAsync(tenantId, new RuleChainId(entityId.getId()));
}
return FluentFuture.from(future).transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return ruleChainDao.countByTenantId(tenantId);
@ -919,18 +932,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ComponentClusteringMode nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(),
"org.thingsboard.rule.engine.api.RuleNode", "clusteringMode");
switch (nodeConfigType) {
case ENABLED:
singletonMode = false;
break;
case SINGLETON:
singletonMode = true;
break;
case USER_PREFERENCE:
default:
singletonMode = ruleNode.isSingletonMode();
break;
}
singletonMode = switch (nodeConfigType) {
case ENABLED -> false;
case SINGLETON -> true;
default -> ruleNode.isSingletonMode();
};
} catch (Exception e) {
log.warn("Failed to get clustering mode: {}", ExceptionUtils.getRootCauseMessage(e));
singletonMode = false;

6
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
@ -52,11 +53,14 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
@Override
protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) {
if (calculatedField.getType() == CalculatedFieldType.ALARM) {
return;
}
long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity);
if (maxCFsPerEntity <= 0) {
return;
}
if (calculatedFieldDao.countCFByEntityId(tenantId, calculatedField.getEntityId()) >= maxCFsPerEntity) {
if (calculatedFieldDao.countByEntityIdAndTypeNot(tenantId, calculatedField.getEntityId(), CalculatedFieldType.ALARM) >= maxCFsPerEntity) {
throw new DataValidationException("Calculated fields per entity limit reached!");
}
}

9
dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.settings;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -31,6 +32,8 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@Service
@Slf4j
public class AdminSettingsServiceImpl implements AdminSettingsService {
@ -106,6 +109,12 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId()));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(adminSettingsDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.ADMIN_SETTINGS;

2
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java

@ -46,6 +46,6 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
List<CalculatedFieldEntity> removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
long countByTenantIdAndEntityId(UUID tenantId, UUID entityId);
long countByTenantIdAndEntityIdAndTypeNot(UUID tenantId, UUID entityId, String type);
}

4
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -98,8 +98,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
}
@Override
public long countCFByEntityId(TenantId tenantId, EntityId entityId) {
return calculatedFieldRepository.countByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
public long countByEntityIdAndTypeNot(TenantId tenantId, EntityId entityId, CalculatedFieldType type) {
return calculatedFieldRepository.countByTenantIdAndEntityIdAndTypeNot(tenantId.getId(), entityId.getId(), type.name());
}
@Override

5
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java

@ -136,6 +136,11 @@ public class JpaUserDao extends JpaAbstractDao<UserEntity, User> implements User
DaoUtil.toPageable(pageLink)));
}
@Override
public int countTenantAdmins(UUID tenantId) {
return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN);
}
@Override
public Long countByTenantId(TenantId tenantId) {
return userRepository.countByTenantId(tenantId.getId());

2
dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java

@ -78,4 +78,6 @@ public interface UserRepository extends JpaRepository<UserEntity, UUID> {
"u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " +
"FROM UserEntity u WHERE u.id > :id ORDER BY u.id")
List<UserFields> findNextBatch(@Param("id") UUID id, Limit limit);
int countByTenantIdAndAuthority(UUID tenantId, Authority authority);
}

30
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.tenant;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,6 +45,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.common.util.DebugModeUtil.DEBUG_MODE_DEFAULT_DURATION_MINUTES;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -230,23 +232,29 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService<Tenant
return Optional.ofNullable(findTenantProfileById(tenantId, new TenantProfileId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(tenantProfileDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.TENANT_PROFILE;
}
private final PaginatedRemover<String, TenantProfile> tenantProfilesRemover =
new PaginatedRemover<>() {
private final PaginatedRemover<String, TenantProfile> tenantProfilesRemover = new PaginatedRemover<>() {
@Override
protected PageData<TenantProfile> findEntities(TenantId tenantId, String id, PageLink pageLink) {
return tenantProfileDao.findTenantProfiles(tenantId, pageLink);
}
@Override
protected PageData<TenantProfile> findEntities(TenantId tenantId, String id, PageLink pageLink) {
return tenantProfileDao.findTenantProfiles(tenantId, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, TenantProfile entity) {
removeTenantProfile(tenantId, entity, entity.isDefault());
}
@Override
protected void removeEntity(TenantId tenantId, TenantProfile entity) {
removeTenantProfile(tenantId, entity, entity.isDefault());
}
};
};
}

10
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.tenant;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -51,6 +52,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TenantDaoService")
@ -86,8 +88,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired
protected TbTransactionalCache<TenantId, Boolean> existsTenantCache;
@TransactionalEventListener(classes = TenantEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(TenantEvictEvent event) {
TenantId tenantId = event.getTenantId();
cache.evict(tenantId);
@ -246,6 +248,12 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
return Optional.ofNullable(findTenantById(TenantId.fromUUID(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findTenantByIdAsync(tenantId, TenantId.fromUUID(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.TENANT;

11
dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.usagerecord;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -48,11 +49,13 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("ApiUsageStateDaoService")
@Slf4j
public class ApiUsageStateServiceImpl extends AbstractEntityService implements ApiUsageStateService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
private final ApiUsageStateDao apiUsageStateDao;
@ -161,7 +164,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
public ApiUsageState update(ApiUsageState apiUsageState) {
log.trace("Executing save [{}]", apiUsageState.getTenantId());
validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id);
validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!");
validateId(apiUsageState.getId(), __ -> "Can't save new usage state. Only update is allowed!");
apiUsageState.setVersion(null);
ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId())
@ -195,6 +198,12 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A
return Optional.ofNullable(findApiUsageStateById(tenantId, new ApiUsageStateId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(apiUsageStateDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Transactional
@Override
public void deleteByTenantId(TenantId tenantId) {

1
dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java

@ -101,4 +101,5 @@ public interface UserDao extends Dao<User>, TenantEntityDao<User> {
PageData<User> findByAuthorityAndTenantProfilesIds(Authority authority, List<TenantProfileId> tenantProfilesIds, PageLink pageLink);
int countTenantAdmins(UUID tenantId);
}

15
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.user;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -79,6 +80,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -112,8 +114,8 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
private final EntityCountService countService;
private final JpaExecutorService executor;
@TransactionalEventListener(classes = UserCacheEvictEvent.class)
@Override
@TransactionalEventListener
public void handleEvictEvent(UserCacheEvictEvent event) {
List<UserCacheKey> keys = new ArrayList<>(2);
keys.add(new UserCacheKey(event.tenantId(), event.newEmail()));
@ -494,6 +496,11 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
}
}
@Override
public int countTenantAdmins(TenantId tenantId) {
return userDao.countTenantAdmins(tenantId.getId());
}
private Optional<UserMobileSessionInfo> findMobileSessionInfo(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE))
.map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class));
@ -646,6 +653,12 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
return Optional.ofNullable(findUserById(tenantId, new UserId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(findUserByIdAsync(tenantId, new UserId(entityId.getId())))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public long countByTenantId(TenantId tenantId) {
return userDao.countByTenantId(tenantId);

12
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.widget;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -47,6 +48,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateIds;
@Service("WidgetTypeDaoService")
@ -54,8 +56,6 @@ import static org.thingsboard.server.dao.service.Validator.validateIds;
public class WidgetTypeServiceImpl implements WidgetTypeService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias ";
public static final String INCORRECT_WIDGETS_BUNDLE_ID = "Incorrect widgetsBundleId ";
@Autowired
@ -281,6 +281,12 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return Optional.ofNullable(findWidgetTypeById(tenantId, new WidgetTypeId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(widgetTypeDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.WIDGET_TYPE;
@ -303,6 +309,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) {
deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId()));
}
};
private final PaginatedRemover<WidgetsBundleId, WidgetTypeInfo> bundleWidgetTypesRemover = new PaginatedRemover<>() {
@ -316,6 +323,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
protected void removeEntity(TenantId tenantId, WidgetTypeInfo widgetTypeInfo) {
deleteWidgetType(tenantId, widgetTypeInfo.getId());
}
};
}

48
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.widget;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@ -33,12 +34,10 @@ import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -48,13 +47,15 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation;
@Service("WidgetsBundleDaoService")
@Slf4j
public class WidgetsBundleServiceImpl implements WidgetsBundleService {
private static final int DEFAULT_WIDGETS_BUNDLE_LIMIT = 300;
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_PAGE_LINK = "Incorrect page link ";
@Autowired
private WidgetsBundleDao widgetsBundleDao;
@ -71,9 +72,6 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
@Autowired
private ImageService imageService;
@Autowired
private ResourceService resourceService;
@Override
public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) {
log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId);
@ -92,9 +90,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
.entityId(result.getId()).created(widgetsBundle.getId() == null).build());
return result;
} catch (Exception e) {
AbstractCachedEntityService.checkConstraintViolation(e,
"uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!");
AbstractCachedEntityService.checkConstraintViolation(e, "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!");
checkConstraintViolation(e,
"uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!",
"widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!");
throw e;
}
}
@ -248,9 +246,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
}
if (widgetsBundleDescriptor.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptor.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
widgetFqnsArrayJson.forEach(fqnJson -> widgetTypeFqns.add(fqnJson.asText()));
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), widgetTypeFqns);
});
@ -274,23 +270,29 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId())));
}
@Override
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) {
return FluentFuture.from(widgetsBundleDao.findByIdAsync(tenantId, entityId.getId()))
.transform(Optional::ofNullable, directExecutor());
}
@Override
public EntityType getEntityType() {
return EntityType.WIDGETS_BUNDLE;
}
private PaginatedRemover<TenantId, WidgetsBundle> tenantWidgetsBundleRemover =
new PaginatedRemover<TenantId, WidgetsBundle>() {
private final PaginatedRemover<TenantId, WidgetsBundle> tenantWidgetsBundleRemover = new PaginatedRemover<>() {
@Override
protected PageData<WidgetsBundle> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink);
}
@Override
protected PageData<WidgetsBundle> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, WidgetsBundle entity) {
deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId()));
}
@Override
protected void removeEntity(TenantId tenantId, WidgetsBundle entity) {
deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId()));
}
};
};
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java

@ -41,6 +41,9 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CREATED;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_SEVERITY_UPDATED;
import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_UPDATED;
import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED;
import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED;
import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT;
@ -78,7 +81,7 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
}
protected S buildEvent(TbMsg msg, TbContext ctx) {
if (msg.isTypeOf(ALARM)) {
if (msg.isTypeOneOf(ALARM, ALARM_CREATED, ALARM_UPDATED, ALARM_SEVERITY_UPDATED)) {
EdgeEventActionType actionType = getAlarmActionType(msg);
return buildEvent(ctx.getTenantId(), actionType, getUUIDFromMsgData(msg), getAlarmEventType(), null);
} else if (msg.isTypeOneOf(ALARM_ACK, ALARM_CLEAR)) {
@ -182,7 +185,8 @@ public abstract class AbstractTbMsgPushNode<T extends BaseTbMsgPushNodeConfigura
protected boolean isSupportedMsgType(TbMsg msg) {
return msg.isTypeOneOf(POST_TELEMETRY_REQUEST, POST_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, TIMESERIES_UPDATED,
ALARM, ALARM_ACK, ALARM_CLEAR, CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT, TO_SERVER_RPC_REQUEST);
ALARM, ALARM_CREATED, ALARM_UPDATED, ALARM_SEVERITY_UPDATED, ALARM_ACK, ALARM_CLEAR, CONNECT_EVENT,
DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT, TO_SERVER_RPC_REQUEST);
}
}

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java

@ -58,15 +58,9 @@ public abstract class TbAbstractGetEntityDataNode<T extends EntityId> extends Tb
protected void processDataAndTell(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) {
DataToFetch dataToFetch = config.getDataToFetch();
switch (dataToFetch) {
case ATTRIBUTES:
processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
break;
case LATEST_TELEMETRY:
processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
break;
case FIELDS:
processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true);
break;
case ATTRIBUTES -> processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
case LATEST_TELEMETRY -> processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
case FIELDS -> processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true);
}
}

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java

@ -17,8 +17,7 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.base.Function;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
@ -54,12 +53,12 @@ public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeC
protected abstract C loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
protected <I extends EntityId> AsyncFunction<I, I> checkIfEntityIsPresentOrThrow(String message) {
protected <I extends EntityId> Function<I, I> checkIfEntityIsPresentOrThrow(String message) {
return id -> {
if (id == null || id.isNullUid()) {
return Futures.immediateFailedFuture(new NoSuchElementException(message));
throw new NoSuchElementException(message);
}
return Futures.immediateFuture(id);
return id;
};
}

26
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -16,19 +16,22 @@
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import java.util.NoSuchElementException;
import static com.google.common.util.concurrent.Futures.immediateFuture;
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "customer attributes",
@ -44,8 +47,6 @@ import org.thingsboard.server.common.data.util.TbPair;
)
public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode<CustomerId> {
private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s";
@Override
protected TbGetEntityDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetEntityDataNodeConfiguration.class);
@ -56,10 +57,19 @@ public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode<Cust
@Override
protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
return Futures.transformAsync(EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator),
checkIfEntityIsPresentOrThrow(String.format(CUSTOMER_NOT_FOUND_MESSAGE, originator.getId(), originator.getEntityType().getNormalName())),
ctx.getDbCallbackExecutor()
);
if (originator.getEntityType() == EntityType.CUSTOMER) {
return immediateFuture((CustomerId) originator);
}
return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), originator)
.transform(customerIdOpt -> {
if (customerIdOpt.isEmpty()) {
throw new NoSuchElementException("Originator not found");
}
if (customerIdOpt.get().isNullUid()) {
throw new IllegalStateException("Originator is not assigned to any customer");
}
return customerIdOpt.get();
}, ctx.getDbCallbackExecutor());
}
@Override

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java

@ -55,7 +55,7 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDevice
@Override
protected ListenableFuture<DeviceId> findEntityIdAsync(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(
return Futures.transform(
EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery()),
checkIfEntityIsPresentOrThrow(RELATED_DEVICE_NOT_FOUND_MESSAGE),
ctx.getDbCallbackExecutor());

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java

@ -58,7 +58,7 @@ public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode<Entit
@Override
public ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
var relatedAttrConfig = (TbGetRelatedDataNodeConfiguration) config;
return Futures.transformAsync(
return Futures.transform(
EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, relatedAttrConfig.getRelationsQuery()),
checkIfEntityIsPresentOrThrow(RELATED_ENTITY_NOT_FOUND_MESSAGE),
ctx.getDbCallbackExecutor());

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesAlarmOriginatorIdAsyncLoader;
import org.thingsboard.rule.engine.util.EntitiesByNameAndTypeLoader;
import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
@ -36,6 +35,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
import java.util.NoSuchElementException;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY;
import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED;
@ -74,16 +74,20 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode<TbChangeOrig
if (newOriginator == null || newOriginator.isNullUid()) {
return Futures.immediateFailedFuture(new NoSuchElementException("Failed to find new originator!"));
}
return Futures.immediateFuture(List.of(ctx.transformMsgOriginator(msg, newOriginator)));
return immediateFuture(List.of(ctx.transformMsgOriginator(msg, newOriginator)));
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, TbMsg msg) {
switch (config.getOriginatorSource()) {
case CUSTOMER:
return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, msg.getOriginator());
if (msg.getOriginator().getEntityType() == EntityType.CUSTOMER) {
return immediateFuture(msg.getOriginator());
}
return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), msg.getOriginator())
.transform(customerIdOpt -> customerIdOpt.orElse(null), ctx.getDbCallbackExecutor());
case TENANT:
return Futures.immediateFuture(ctx.getTenantId());
return immediateFuture(ctx.getTenantId());
case RELATED:
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, msg.getOriginator(), config.getRelationsQuery());
case ALARM_ORIGINATOR:
@ -93,7 +97,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode<TbChangeOrig
String entityName = TbNodeUtils.processPattern(config.getEntityNamePattern(), msg);
try {
EntityId targetEntity = EntitiesByNameAndTypeLoader.findEntityId(ctx, entityType, entityName);
return Futures.immediateFuture(targetEntity);
return immediateFuture(targetEntity);
} catch (IllegalStateException e) {
return Futures.immediateFailedFuture(e);
}

50
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java

@ -1,50 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
public class EntitiesCustomerIdAsyncLoader {
public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId originator) {
switch (originator.getEntityType()) {
case CUSTOMER:
return Futures.immediateFuture((CustomerId) originator);
case USER:
return toCustomerIdAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originator));
case ASSET:
return toCustomerIdAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originator));
case DEVICE:
return toCustomerIdAsync(ctx, Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originator)));
default:
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originator.getEntityType()));
}
}
private static <T extends HasCustomerId> ListenableFuture<CustomerId> toCustomerIdAsync(TbContext ctx, ListenableFuture<T> future) {
return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor());
}
}

2
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java

@ -28,7 +28,7 @@ public class TestDbCallbackExecutor implements ListeningExecutor {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
return Futures.immediateFailedFuture(e);
}
}

111
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java

@ -16,7 +16,7 @@
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.FluentFuture;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ -53,19 +53,19 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@ -73,19 +73,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbGetCustomerAttributeNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID());
private final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID());
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
@ -93,21 +94,24 @@ public class TbGetCustomerAttributeNodeTest {
@Mock
private TimeseriesService timeseriesServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
private EntityService entityServiceMock;
private TbGetCustomerAttributeNode node;
private TbGetEntityDataNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
@BeforeEach
public void setUp() {
public void setup() {
node = new TbGetCustomerAttributeNode();
config = new TbGetEntityDataNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
lenient().when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
lenient().when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock);
}
@Test
@ -237,12 +241,9 @@ public class TbGetCustomerAttributeNodeTest {
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(null)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(userId));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, userId)).thenReturn(
FluentFuture.from(immediateFuture(Optional.empty()))
);
// WHEN
node.onMsg(ctxMock, msg);
@ -252,18 +253,13 @@ public class TbGetCustomerAttributeNodeTest {
var actualExceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, times(1))
.tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
verify(ctxMock).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
var actualMessage = actualMessageCaptor.getValue();
var actualException = actualExceptionCaptor.getValue();
var expectedExceptionMessage = String.format(
"Failed to find customer for entity with id: %s and type: %s",
userId.getId(), userId.getEntityType().getNormalName());
assertEquals(msg, actualMessage);
assertEquals(expectedExceptionMessage, actualException.getMessage());
assertEquals("Originator not found", actualException.getMessage());
assertInstanceOf(NoSuchElementException.class, actualException);
}
@ -282,16 +278,12 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId()));
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).thenReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
.thenReturn(immediateFuture(attributesList));
// WHEN
node.onMsg(ctxMock, msg);
@ -299,7 +291,7 @@ public class TbGetCustomerAttributeNodeTest {
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
@ -329,16 +321,12 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId()));
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, user.getId())).thenReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
.thenReturn(immediateFuture(attributesList));
// WHEN
node.onMsg(ctxMock, msg);
@ -346,7 +334,7 @@ public class TbGetCustomerAttributeNodeTest {
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
@ -375,21 +363,18 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(customer.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseriesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
.thenReturn(immediateFuture(timeseriesList));
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verifyNoInteractions(entityServiceMock);
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
@ -408,7 +393,7 @@ public class TbGetCustomerAttributeNodeTest {
public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() {
// GIVEN
var asset = new Asset(new AssetId(UUID.randomUUID()));
asset.setCustomerId(new CustomerId(UUID.randomUUID()));
asset.setCustomerId(CUSTOMER_ID);
prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId());
@ -419,16 +404,12 @@ public class TbGetCustomerAttributeNodeTest {
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()));
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(asset.getCustomerId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseriesList));
when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, asset.getId())).thenReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(CUSTOMER_ID), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(immediateFuture(timeseriesList));
// WHEN
node.onMsg(ctxMock, msg);
@ -436,7 +417,7 @@ public class TbGetCustomerAttributeNodeTest {
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(

90
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java

@ -15,8 +15,10 @@
*/
package org.thingsboard.rule.engine.transform;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
@ -35,6 +37,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -54,21 +57,24 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.relation.RelationService;
import java.util.Collections;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.lenient;
import static org.thingsboard.rule.engine.transform.OriginatorSource.ALARM_ORIGINATOR;
import static org.thingsboard.rule.engine.transform.OriginatorSource.CUSTOMER;
import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY;
@ -93,16 +99,23 @@ public class TbChangeOriginatorNodeTest {
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private RelationService relationServiceMock;
@Mock
private RuleEngineAlarmService alarmServiceMock;
@Mock
private EntityService entityServiceMock;
@BeforeEach
public void before() throws TbNodeException {
public void setup() {
node = new TbChangeOriginatorNode();
config = new TbChangeOriginatorNodeConfiguration().defaultConfiguration();
lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
lenient().when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
lenient().when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
lenient().when(ctxMock.getAlarmService()).thenReturn(alarmServiceMock);
lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock);
}
@Test
@ -172,8 +185,13 @@ public class TbChangeOriginatorNodeTest {
}
@Test
public void givenOriginatorSourceIsCustomer_whenOnMsg_thenTellSuccess() throws TbNodeException {
Device device = new Device(DEVICE_ID);
@DisplayName("""
Given a device assigned to a customer and node configured to change originator to customer,
when processing the message,
then should change message originator from device to customer""")
public void givenDeviceAssignedToCustomer_whenProcessingMessage_thenChangesOriginatorToCustomer() throws TbNodeException {
// GIVEN
var device = new Device(DEVICE_ID);
device.setCustomerId(CUSTOMER_ID);
TbMsg msg = TbMsg.newMsg()
@ -186,16 +204,52 @@ public class TbChangeOriginatorNodeTest {
.originator(CUSTOMER_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getDeviceService()).willReturn(deviceServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(deviceServiceMock.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(device);
given(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).willReturn(
FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID)))
);
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
// WHEN
node.onMsg(ctxMock, msg);
// THEN
then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID);
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().tellSuccess(actualMsg.capture());
assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg);
}
@Test
@DisplayName("""
Given a customer as message originator and node configured to change originator to customer,
when processing the message,
then should keep the customer as originator""")
public void givenCustomerAsOriginator_whenProcessingMessage_thenKeepsCustomerAsOriginator() throws TbNodeException {
// GIVEN
var customer = new Customer(CUSTOMER_ID);
var msg = TbMsg.newMsg()
.type(TbMsgType.POST_TELEMETRY_REQUEST)
.originator(customer.getId())
.metaData(TbMsgMetaData.EMPTY)
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
var expectedMsg = msg.transform()
.originator(CUSTOMER_ID)
.build();
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
// WHEN
node.onMsg(ctxMock, msg);
then(deviceServiceMock).should().findDeviceById(TENANT_ID, DEVICE_ID);
// THEN
then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID);
ArgumentCaptor<TbMsg> actualMsg = ArgumentCaptor.forClass(TbMsg.class);
then(ctxMock).should().tellSuccess(actualMsg.capture());
@ -216,8 +270,6 @@ public class TbChangeOriginatorNodeTest {
.originator(TENANT_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
@ -240,9 +292,6 @@ public class TbChangeOriginatorNodeTest {
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getRelationService()).willReturn(relationServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(relationServiceMock.findByQuery(any(TenantId.class), any(EntityRelationsQuery.class))).willReturn(Futures.immediateFuture(Collections.emptyList()));
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
@ -282,9 +331,6 @@ public class TbChangeOriginatorNodeTest {
.originator(DEVICE_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getAlarmService()).willReturn(alarmServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(alarmServiceMock.findAlarmByIdAsync(any(TenantId.class), any(AlarmId.class))).willReturn(Futures.immediateFuture(alarm));
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
@ -315,9 +361,6 @@ public class TbChangeOriginatorNodeTest {
.originator(ASSET_ID)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getAssetService()).willReturn(assetServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(new Asset(ASSET_ID));
given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg);
@ -355,9 +398,6 @@ public class TbChangeOriginatorNodeTest {
.data(TbMsg.EMPTY_JSON_OBJECT)
.build();
given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor);
given(ctxMock.getAssetService()).willReturn(assetServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(null);
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));

154
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java

@ -1,154 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.user.UserService;
import java.util.EnumSet;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class EntitiesCustomerIdAsyncLoaderTest {
private static final EnumSet<EntityType> SUPPORTED_ENTITY_TYPES = EnumSet.of(
EntityType.CUSTOMER,
EntityType.USER,
EntityType.ASSET,
EntityType.DEVICE
);
private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
private UserService userServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Test
public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var customer = new Customer(new CustomerId(UUID.randomUUID()));
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get();
// THEN
assertEquals(customer.getId(), actualCustomerId);
}
@Test
public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var user = new User(new UserId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
user.setCustomerId(expectedCustomerId);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var asset = new Asset(new AssetId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
asset.setCustomerId(expectedCustomerId);
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var device = new Device(new DeviceId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
device.setCustomerId(expectedCustomerId);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
doReturn(device).when(deviceServiceMock).findDeviceById(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() {
for (var entityType : EntityType.values()) {
if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) {
var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID());
var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType;
var exception = assertThrows(ExecutionException.class,
() -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get());
assertInstanceOf(TbNodeException.class, exception.getCause());
assertEquals(expectedExceptionMsg, exception.getMessage());
}
}
}
}

97
ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch

File diff suppressed because one or more lines are too long

2
ui-ngx/src/app/core/guards/auth.guard.ts

@ -131,7 +131,7 @@ export class AuthGuard {
}
}
if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) {
this.mobileService.handleMobileNavigation(path, params);
this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams);
return of(false);
}
if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) {

2
ui-ngx/src/app/core/http/admin.service.ts

@ -21,9 +21,9 @@ import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
AutoCommitSettings,
MailConfigTemplate,
FeaturesInfo,
JwtSettings,
MailConfigTemplate,
MailServerSettings,
RepositorySettings,
RepositorySettingsInfo,

13
ui-ngx/src/app/core/http/calculated-fields.service.ts

@ -15,11 +15,15 @@
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models';
import {
CalculatedField,
CalculatedFieldTestScriptInputParams,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
@ -46,9 +50,8 @@ export class CalculatedFieldsService {
return this.http.delete<boolean>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
}
public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config));
}
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {

38
ui-ngx/src/app/core/http/git-hub.service.ts

@ -0,0 +1,38 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class GitHubService {
constructor(
private http: HttpClient
) { }
public getGitHubStar(config?: RequestConfig): Observable<number> {
return this.http.get<any>('https://api.github.com/repos/thingsboard/thingsboard', defaultHttpOptionsFromConfig(config)).pipe(
catchError(() => of({})),
map((res: any) => res?.stargazers_count ?? 0)
)
}
}

4
ui-ngx/src/app/core/services/mobile.service.ts

@ -106,9 +106,9 @@ export class MobileService {
}
}
public handleMobileNavigation(path?: string, params?: Params) {
public handleMobileNavigation(path?: string, params?: Params, queryParams?: Params) {
if (this.mobileApp) {
this.mobileChannel.callHandler(navigationHandler, path, params);
this.mobileChannel.callHandler(navigationHandler, path, params, queryParams);
}
}

10
ui-ngx/src/app/core/utils.ts

@ -34,8 +34,10 @@ import {
} from '@shared/models/js-function.models';
import { DomSanitizer } from '@angular/platform-browser';
import { SecurityContext } from '@angular/core';
import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
const varsRegex = /\${([^}]*)}/g;
const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
const scrollSubject = new Subject<Event>();
@ -1033,3 +1035,11 @@ export const trimDefaultValues = (input: Record<string, any>, defaults: Record<s
return result;
}
export const validateEmail = (control: AbstractControl): ValidationErrors | null => {
if (isUndefinedOrNull(control.value) || (typeof control.value === 'string' && control.value.length === 0)) {
return null;
}
return emailRegex.test(control.value) ? null : {email: true};
};

49
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html

@ -0,0 +1,49 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form style="width: 800px">
<mat-toolbar class="justify-between" color="primary">
<h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2>
<button mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="flex flex-1 flex-col">
<mat-form-field class="mat-block" appearance="outline">
<textarea matInput [formControl]="alarmDetailsControl" rows="5"
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
@if (!data.readonly) {
<button mat-raised-button color="primary"
type="button"
(click)="save()"
[disabled]="alarmDetailsControl.invalid || !alarmDetailsControl.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</form>

60
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts

@ -0,0 +1,60 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
export interface AlarmRuleDetailsDialogData {
alarmDetails: string;
readonly: boolean;
}
@Component({
selector: 'tb-edit-alarm-details-dialog',
templateUrl: './alarm-rule-details-dialog.component.html',
providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string> {
alarmDetailsControl: FormControl<string>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData,
public dialogRef: MatDialogRef<AlarmRuleDetailsDialogComponent, string>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.alarmDetailsControl = this.fb.control(this.data.alarmDetails);
if (this.data.readonly) {
this.alarmDetailsControl.disable();
}
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.dialogRef.close(this.alarmDetailsControl.value);
}
}

164
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -0,0 +1,164 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex size-full max-w-4xl flex-col">
<mat-toolbar color="primary">
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2>
<span class="flex-1"></span>
<div tb-help="alarmRule"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content class="flex-1">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'alarm-rule.alarm-type' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) {
<mat-error>
@if (fieldFormGroup.get('name').hasError('required')) {
{{ 'alarm-rule.alarm-type-required' | translate }}
} @else if (fieldFormGroup.get('name').hasError('pattern')) {
{{ 'alarm-rule.alarm-type-pattern' | translate }}
} @else if (fieldFormGroup.get('name').hasError('maxlength')) {
{{ 'alarm-rule.alarm-type-max-length' | translate }}
}
</mat-error>
}
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
</div>
<ng-container formGroupName="configuration">
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="data.entityId"
[tenantId]="data.tenantId"
[ownerId]="data.ownerId"
[entityName]="data.entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-alarm-rules' | translate }}</div>
<div class="flex flex-1 flex-col">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments">
</tb-create-cf-alarm-rules>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.clear-alarm-rule' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value">
<div class="clear-alarm-rule flex flex-1 flex-row">
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments">
</tb-cf-alarm-rule>
</div>
<button mat-icon-button
class="button-icon"
type="button"
(click)="removeClearAlarmRule()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate style="margin: 16px 0"
class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'alarm-rule.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
</button>
</div>
</div>
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="false">
<mat-expansion-panel-header>
{{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
}
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToTenant">
{{ 'alarm-rule.propagate-alarm-to-tenant' | translate }}
</mat-slide-toggle>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty">
{{ data.buttonTitle | translate }}
</button>
</div>
</div>

34
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.calculated-field-dialog-container {
width: 869px;
max-width: 100%;
}
.clear-alarm-rule {
border: 1px solid rgba(0, 0, 0, 0.12);
border-left-width: 4px;
border-left-color: green;
border-radius: 4px;
padding: 8px;
min-width: 0;
}
.button-icon {
color: rgba(0, 0, 0, 0.38);
min-width: 40px;
}

179
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts

@ -0,0 +1,179 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { EntityType } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips";
import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils";
export interface AlarmRuleDialogData {
value?: CalculatedField;
buttonTitle: string;
entityId: EntityId;
tenantId: string;
entityName?: string;
ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
isDirty?: boolean;
}
@Component({
selector: 'tb-alarm-rule-dialog',
templateUrl: './alarm-rule-dialog.component.html',
styleUrls: ['./alarm-rule-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> {
fieldFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
type: [CalculatedFieldType.ALARM],
debugSettings: [],
configuration: this.fb.group({
arguments: this.fb.control({}),
propagate: [false],
propagateToOwner: [false],
propagateToTenant: [false],
propagateRelationTypes: [null],
createRules: [null],
clearRule: [null],
}),
});
additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig,
action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null;
readonly EntityType = EntityType;
readonly CalculatedFieldType = CalculatedFieldType;
readonly ScriptLanguage = ScriptLanguage;
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
private destroyRef: DestroyRef,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.observeIsLoading();
this.applyDialogData();
}
get configFormGroup(): FormGroup {
return this.fieldFormGroup.get('configuration') as FormGroup;
}
get arguments(): Record<string, CalculatedFieldArgument> {
return this.fieldFormGroup.get('configuration.arguments').value;
}
public removeClearAlarmRule() {
this.configFormGroup.patchValue({clearRule: null});
this.fieldFormGroup.markAsDirty();
}
public addClearAlarmRule() {
const clearAlarmRule: AlarmRule = {
condition: {
type: AlarmRuleConditionType.SIMPLE,
expression: {
type: AlarmRuleExpressionType.SIMPLE
}
}
};
this.configFormGroup.patchValue({clearRule: clearAlarmRule});
}
removeRelationType(key: string): void {
const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value;
const index = keys.indexOf(key);
if (index >= 0) {
keys.splice(index, 1);
this.configFormGroup.get('propagateRelationTypes').setValue(keys);
}
}
addRelationType(event: MatChipInputEvent): void {
const input = event.chipInput.inputElement;
let value = (event.value ?? '').trim();
if (value) {
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? [];
if (keys.indexOf(value) === -1) {
keys.push(value);
this.configFormGroup.get('propagateRelationTypes').setValue(keys);
}
}
if (input) {
input.value = '';
}
}
get fromGroupValue(): CalculatedField {
return deepTrim(this.fieldFormGroup.value as CalculatedField);
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
if (this.fieldFormGroup.valid) {
const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue};
alarmRule.configuration.type = CalculatedFieldType.ALARM;
this.calculatedFieldsService.saveCalculatedField(alarmRule)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(calculatedField => this.dialogRef.close(calculatedField));
}
}
private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
}
private observeIsLoading(): void {
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => {
if (loading) {
this.fieldFormGroup.disable({emitEvent: false});
} else {
this.fieldFormGroup.enable({emitEvent: false});
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
}
});
}
}

80
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts

@ -0,0 +1,80 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component";
import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component";
import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component";
import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component";
import {
CfAlarmRuleConditionDialogComponent
} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component";
import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component";
import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import {
EntityDebugSettingsButtonComponent
} from "@home/components/entity/debug/entity-debug-settings-button.component";
import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component";
import {
CalculatedFieldArgumentsTableModule
} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module";
import {
AlarmRuleFilterPredicateListComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component";
import {
AlarmRuleFilterPredicateComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component";
import {
AlarmRuleFilterPredicateValueComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component";
import {
AlarmRuleComplexFilterPredicateDialogComponent
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component";
import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
@NgModule({
declarations: [
AlarmRuleDialogComponent,
CreateCfAlarmRulesComponent,
CfAlarmRuleComponent,
CfAlarmRuleConditionComponent,
CfAlarmRuleConditionDialogComponent,
CfAlarmScheduleComponent,
CfAlarmScheduleDialogComponent,
AlarmRuleFilterTextComponent,
AlarmRuleFilterListComponent,
AlarmRuleFilterDialogComponent,
AlarmRuleFilterPredicateListComponent,
AlarmRuleFilterPredicateComponent,
AlarmRuleFilterPredicateValueComponent,
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
],
imports: [
CommonModule,
SharedModule,
EntityDebugSettingsButtonComponent,
CalculatedFieldArgumentsTableModule
],
exports: [
AlarmRuleDialogComponent,
]
})
export class AlarmRuleModule { }

278
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -0,0 +1,278 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import {
ArgumentEntityType,
CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldType,
} from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import {
AlarmRuleDialogComponent,
AlarmRuleDialogData
} from "@home/components/alarm-rules/alarm-rule-dialog.component";
import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldDebugDialogData
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
export class AlarmRulesTableConfig extends EntityTableConfig<any> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'),
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
};
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
private datePipe: DatePipe,
public entityId: EntityId = null,
private store: Store<AppState>,
private destroyRef: DestroyRef,
private renderer: Renderer2,
public entityName: string,
private ownerId: EntityId = null,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
) {
super();
this.tableTitle = this.translate.instant('alarm-rule.alarm-rules');
this.detailsPanelEnabled = false;
this.pageMode = false;
this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = {
type: 'alarm-rule.alarm-rule',
typePlural: 'alarm-rule.alarm-rules',
list: 'alarm-rule.list',
add: 'action.add',
noEntities: 'alarm-rule.no-found',
search: 'action.search',
selectedEntities: 'alarm-rule.selected-fields'
};
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.getCalculatedAlarmDialog.bind(this);
this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.addActionDescriptors = [
{
name: this.translate.instant('alarm-rule.create'),
icon: 'insert_drive_file',
isEnabled: () => true,
onAction: ($event) => this.getTable().addEntity($event)
},
{
name: this.translate.instant('alarm-rule.import'),
icon: 'file_upload',
isEnabled: () => true,
onAction: () => this.importCalculatedField()
}
];
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.columns.push(new DateEntityTableColumn<CalculatedFieldAlarmRule>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', '33%'));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', '67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '60px',
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: '0 14px'}}, false));
this.cellActionDescriptors.push(
{
name: this.translate.instant('action.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: (event$, entity) => this.exportAlarmRule(event$, entity),
},
{
name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock',
isEnabled: () => true,
onAction: (_, entity) => this.openDebugEventsDialog(entity),
},
{
name: '',
nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings),
icon: 'mdi:bug',
isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
},
{
name: this.translate.instant('action.edit'),
icon: 'edit',
isEnabled: () => true,
onAction: (_, entity) => this.editCalculatedField(entity),
}
);
}
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM);
}
onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = {
...this.additionalDebugActionConfig,
action: () => this.openDebugEventsDialog(calculatedField)
};
if ($event) {
$event.stopPropagation();
}
const { viewContainerRef, renderer } = this.entityDebugSettingsService;
if (!viewContainerRef || !renderer) {
this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef;
this.entityDebugSettingsService.renderer = this.renderer;
}
this.entityDebugSettingsService.openDebugStrategyPanel({
debugSettings,
debugConfig: {
entityType: EntityType.CALCULATED_FIELD,
entityLabel: 'alarm-rule.alarm-rule',
additionalActionConfig,
},
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
}, $event.target as Element);
}
private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty)
.subscribe((res) => {
if (res) {
this.updateData();
}
});
}
private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
return this.dialog.open<AlarmRuleDialogComponent, AlarmRuleDialogData, CalculatedField>(AlarmRuleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle,
entityId: this.entityId,
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
additionalDebugActionConfig: this.additionalDebugActionConfig,
isDirty,
},
enterAnimationDuration: isDirty ? 0 : null,
})
.afterClosed()
.pipe(filter(Boolean));
}
private openDebugEventsDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
tenantId: this.tenantId,
value: calculatedField,
getTestScriptDialogFn: null,
}
})
.afterClosed()
.subscribe();
}
private exportAlarmRule($event: Event, calculatedField: CalculatedField): void {
if ($event) {
$event.stopPropagation();
}
this.importExportService.exportCalculatedField(calculatedField.id.id);
}
private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
filter(Boolean),
switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
filter(Boolean),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => this.updateData());
}
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
if (calculatedField.type === CalculatedFieldType.GEOFENCING) {
calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => {
const arg = calculatedField.configuration.zoneGroups[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
: arg;
return acc;
}, {});
} else {
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const arg = calculatedField.configuration.arguments[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
: arg;
return acc;
}, {});
}
return calculatedField;
}
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
this.calculatedFieldsService.getCalculatedFieldById(id).pipe(
switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })),
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData());
}
}

20
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html

@ -0,0 +1,20 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
@if (alarmRulesTableConfig) {
<tb-entities-table [entitiesTableConfig]="alarmRulesTableConfig"></tb-entities-table>
}

20
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
tb-entities-table {
--mat-sidenav-content-background-color: white;
}
}

88
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts

@ -0,0 +1,88 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
effect,
input,
Renderer2,
ViewChild,
} from '@angular/core';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
@Component({
selector: 'tb-alarm-rules-table',
templateUrl: './alarm-rules-table.component.html',
styleUrls: ['./alarm-rules-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [EntityDebugSettingsService]
})
export class AlarmRulesTableComponent {
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
active = input<boolean>();
entityId = input<EntityId>();
entityName = input<string>();
ownerId = input<EntityId>();
alarmRulesTableConfig: AlarmRulesTableConfig;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
private store: Store<AppState>,
private datePipe: DatePipe,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private destroyRef: DestroyRef) {
effect(() => {
if (this.active()) {
this.alarmRulesTableConfig = new AlarmRulesTableConfig(
this.calculatedFieldsService,
this.translate,
this.dialog,
this.datePipe,
this.entityId(),
this.store,
this.destroyRef,
this.renderer,
this.entityName(),
this.ownerId(),
this.importExportService,
this.entityDebugSettingsService,
);
this.cd.markForCheck();
}
});
}
}

188
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -0,0 +1,188 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}</h2>
<span class="flex-1"></span>
<div formGroupName="expression" class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;">
<tb-toggle-select formControlName="type" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="AlarmRuleExpressionType.SIMPLE">{{ 'alarm-rule.expression-type.simple' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="AlarmRuleExpressionType.TBEL">{{ 'alarm-rule.expression-type.tbel' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-padding no-border">
<div class="flex flex-1 flex-col" formGroupName="expression">
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-alarm-rule-filter-list
[operation]="conditionFormGroup.get('expression.operation').value"
[arguments]="arguments"
formControlName="filters">
</tb-alarm-rule-filter-list>
</div>
} @else {
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">
{{ 'alarm-rule.script' | translate }}
</div>
<tb-js-func formControlName="expression"
functionName="expression"
[disableUndefinedCheck]="true"
[scriptLanguage]="scriptLanguage.TBEL"
[functionArgs]="functionArgs"
[highlightRules]="argumentsHighlightRules"
[editorCompleter]="argumentsEditorCompleter"
noValidate="true">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.tbel' | translate }}
</div>
</tb-js-func>
</div>
}
</div>
<section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
<tb-toggle-select [formControl]="durationDynamicModeControl"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="flex flex-row gap-2 xs:flex-col">
<div class="flex-1" [class.!hidden]="durationDynamicModeControl.value">
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!durationDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'value'}"></ng-container>
</div>
<div class="max-w-23% flex-full xs:max-w-full">
<mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="unit" required
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('unit').hasError('required')">
{{ 'alarm-rule.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
} @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) {
<div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
<tb-toggle-select [formControl]="repeatingDynamicModeControl" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="flex flex-1 flex-row gap-2">
<div class="flex-1" [class.!hidden]="repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container>
</div>
</div>
</div>
}
</section>
</div>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"
type="button"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
<ng-template #staticValueTemplate let-type="type" let-groupName="groupName">
<div class="flex flex-1 flex-row items-center gap-2" [formGroupName]="groupName">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input type="number" matInput
step="1" min="1" max="2147483647"
formControlName="staticValue">
<mat-label>{{ defaultValuePlaceholder | translate }}</mat-label>
@if (conditionFormGroup.get(groupName).get('staticValue').hasError('required')) {
<mat-error>{{ defaultValueRequiredError | translate }}</mat-error>
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('min')) {
<mat-error>{{ defaultValueRangeError | translate }}</mat-error>
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('max')) {
<mat-error>{{ defaultValueRangeError | translate }}</mat-error>
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) {
<mat-error>{{ defaultValuePatternError | translate }}</mat-error>
}
</mat-form-field>
</div>
</ng-template>
<ng-template #dynamicValueTemplate let-type="type" let-groupName="groupName">
<ng-container [formGroupName]="groupName">
<mat-form-field class="flex flex-1" appearance="outline">
<mat-label translate>alarm-rule.value-argument</mat-label>
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')">
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
</mat-form-field>
</ng-container>
</ng-template>
</form>

226
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts

@ -0,0 +1,226 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import {
CalculatedFieldArgument,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights
} from "@shared/models/calculated-field.models";
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmRuleCondition;
arguments?: Record<string, CalculatedFieldArgument>;
}
@Component({
selector: 'tb-cf-alarm-rule-condition-dialog',
templateUrl: './cf-alarm-rule-condition-dialog.component.html',
providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition> {
AlarmRuleExpressionType = AlarmRuleExpressionType;
timeUnits = Object.values(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.values(AlarmRuleConditionType);
AlarmConditionType = AlarmRuleConditionType;
alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap;
readonly = this.data.readonly;
condition = this.data.condition;
conditionFormGroup = this.fb.group({
expression: this.fb.group({
type: [AlarmRuleExpressionType.SIMPLE],
expression: ['', [Validators.required]],
operation: [ComplexOperation.AND],
filters: [null],
}),
type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required),
unit: this.fb.control(TimeUnit.SECONDS, Validators.required),
value: this.fb.group({
staticValue: this.fb.control<number | null>(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]),
dynamicValueArgument: this.fb.control<string>('', Validators.required),
}),
count: this.fb.group({
staticValue: this.fb.control<number | null>(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]),
dynamicValueArgument: this.fb.control<string>('', Validators.required),
}),
});
readonly scriptLanguage = ScriptLanguage;
defaultValuePlaceholder = '';
defaultValueRequiredError = '';
defaultValueRangeError = '';
defaultValuePatternError = '';
durationDynamicModeControl = this.fb.control<boolean>(false);
repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
functionArgs: Array<string>;
argumentsEditorCompleter: TbEditorCompleter;
argumentsHighlightRules: AceHighlightRules;
arguments = this.data.arguments;
argumentsList: Array<string>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments);
this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments);
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.conditionFormGroup.patchValue({
expression: {
type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
expression: this.condition?.expression?.expression ?? null,
filters: this.condition?.expression?.filters ?? [],
operation: this.condition?.expression?.operation ?? ComplexOperation.AND
},
type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
staticValue: this.condition?.value?.staticValue,
dynamicValueArgument: this.condition?.value?.dynamicValueArgument,
},
count: {
staticValue: this.condition?.count?.staticValue ?? null,
dynamicValueArgument: this.condition?.count?.dynamicValueArgument
}
}, {emitEvent: false});
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
this.conditionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type) => {
this.updateValidators(type, true);
});
this.conditionFormGroup.get('expression.type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type) => {
this.updateExpressionTypeValidator(type);
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
});
this.durationDynamicModeControl.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((mode) => {
this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode);
});
this.repeatingDynamicModeControl.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((mode) => {
this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode);
});
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
}
updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) {
const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count');
if (dynamicValue) {
control.get('staticValue').disable({emitEvent: false});
control.get('dynamicValueArgument').enable({emitEvent: false});
} else {
control.get('staticValue').enable({emitEvent: false});
control.get('dynamicValueArgument').disable({emitEvent: false});
}
}
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false});
} else {
this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false});
this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false});
}
}
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) {
switch (type) {
case AlarmRuleConditionType.DURATION:
this.conditionFormGroup.get('unit').enable({emitEvent: false});
this.conditionFormGroup.get('value').enable({emitEvent: false});
this.conditionFormGroup.get('count').disable({emitEvent: false});
this.updateStaticValueValidator(type, this.durationDynamicModeControl.value);
this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value';
this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required';
this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range';
this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern';
break;
case AlarmRuleConditionType.REPEATING:
this.conditionFormGroup.get('count').enable({emitEvent: false});
this.conditionFormGroup.get('value').disable({emitEvent: false});
this.conditionFormGroup.get('unit').disable({emitEvent: false});
this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value);
this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value';
this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required';
this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range';
this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern';
break;
case AlarmRuleConditionType.SIMPLE:
this.conditionFormGroup.get('value').disable({emitEvent: false});
this.conditionFormGroup.get('count').disable({emitEvent: false});
this.conditionFormGroup.get('unit').disable({emitEvent: false});
break;
}
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
}
}

52
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -0,0 +1,52 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-alarm-rule-condition flex flex-col gap-4 min-w-0" [formGroup]="alarmRuleConditionFormGroup">
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.condition' | translate }}</div>
<button [disabled]="disabled"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() ? 'primary' : 'warn'"
(click)="openFilterDialog($event)">
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="true"
[specText]="specText"
required
addFilterPrompt="{{'alarm-rule.enter-alarm-rule-condition-prompt' | translate}}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
</div>
</button>
</div>
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div>
<button [disabled]="disabled"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button color="primary"
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
</div>
</button>
</div>
</div>

37
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: flex;
flex: 1;
.tb-alarm-rule-condition {
display: flex;
flex: 1;
&-button {
--mat-outlined-button-horizontal-padding: 3px 0px 12px;
display: block;
width: 100%;
}
&-label {
display: block;
text-align: start;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save