Browse Source

Merge remote-tracking branch 'origin/master' into feature/broadcast-logs-improvement

pull/9083/head
Andrii Landiak 3 years ago
parent
commit
cca7a87c19
  1. 40
      .github/workflows/check-configuration-files.yml
  2. 13
      application/src/main/data/json/system/widget_bundles/weather_widgets.json
  3. 29
      application/src/main/data/json/system/widget_types/wind_speed_and_direction.json
  4. 6
      application/src/main/data/upgrade/3.6.0/schema_update.sql
  5. 2
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  6. 12
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  7. 2
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  8. 2
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  9. 2
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  10. 20
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  11. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
  13. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
  15. 1
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  16. 30
      application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java
  17. 10
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  18. 80
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  19. 1
      application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java
  20. 4
      application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java
  21. 648
      application/src/main/resources/thingsboard.yml
  22. 65
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  23. 36
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  24. 140
      application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java
  25. 37
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  26. 4
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  27. 28
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  28. 8
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  29. 64
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java
  30. 31
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  31. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
  32. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  33. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  34. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiLimitService.java
  35. 69
      common/data/src/main/java/org/thingsboard/server/common/data/EntitySubtype.java
  36. 13
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  37. 2
      common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java
  38. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  39. 11
      common/data/src/test/java/org/thingsboard/server/common/data/StringUtilsTest.java
  40. 16
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java
  41. 4
      common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java
  42. 27
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  43. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  44. 140
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  45. 46
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  46. 17
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  47. 4
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java
  48. 25
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  49. 3
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java
  50. 9
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  51. 50
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  52. 2
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  53. 2
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java
  54. 5
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  55. 8
      dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
  56. 5
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java
  57. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java
  58. 3
      dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java
  59. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java
  60. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  61. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java
  62. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java
  63. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java
  64. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java
  65. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  66. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  67. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  68. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java
  69. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java
  70. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java
  71. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java
  72. 3
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  73. 34
      dao/src/main/java/org/thingsboard/server/dao/usagerecord/DefaultApiLimitService.java
  74. 3
      dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java
  75. 8
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  76. 4
      dao/src/main/resources/sql/schema-entities-idx.sql
  77. 4
      dao/src/main/resources/sql/schema-entities.sql
  78. 11
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java
  79. 52
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  80. 50
      dao/src/test/java/org/thingsboard/server/dao/service/TbCacheSerializationTest.java
  81. 1
      pull_request_template.md
  82. 26
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  83. 122
      tools/src/main/python/check_yml_file.py
  84. 1
      ui-ngx/package.json
  85. 4
      ui-ngx/src/app/core/utils.ts
  86. 28
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  87. 2
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  88. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  89. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html
  90. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  91. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html
  92. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html
  93. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts
  94. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  95. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  96. 37
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  97. 25
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  98. 138
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  99. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  100. 17
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

40
.github/workflows/check-configuration-files.yml

@ -0,0 +1,40 @@
#
# Copyright © 2016-2023 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.
#
name: Check configuration files
on:
push:
branches:
- master
pull_request:
paths:
- 'application/src/main/resources/thingsboard.yml'
jobs:
build:
name: Check thingsboard.yml file
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10.2"
architecture: "x64"
env:
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache
- name: Run Verification Script
run: python3 tools/src/main/python/check_yml_file.py

13
application/src/main/data/json/system/widget_bundles/weather_widgets.json

@ -0,0 +1,13 @@
{
"widgetsBundle": {
"alias": "weather_widgets",
"title": "Weather widgets",
"image": null,
"description": null,
"externalId": null,
"name": "Weather widgets"
},
"widgetTypeFqns": [
"wind_speed_and_direction"
]
}

29
application/src/main/data/json/system/widget_types/wind_speed_and_direction.json

@ -0,0 +1,29 @@
{
"fqn": "wind_speed_and_direction",
"name": "Wind speed and direction",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAilBMVEXg4ODf39/g4OAAAADg4ODf39/////g4OD7+/v39/fn5+f09PTx8fEhISHr6+vu7u7k5OSenp7i4uLOzs6qqqqQkJDt7e3IyMjw8PDp6em2trbV1dU8PDwvLy/b29vCwsJXV1eCgoKGhoa6urptbW10dHRKSkqkpKRmZma8vLywsLCSkpJ7e3thYWHl47mIAAAABnRSTlPvIL8Ar7DvmsykAAAHPklEQVR42uzZbY+aQBSGYfuSh+ecmWFgRFBBq2m2u9v0//++grtGq9uUioIa70Txk3o5c8wYR18+fR5Ft97XT19Gn0aeuPFoa8ZXizvI19vq5tejiaNRhLsoekCurAfk2npArq0H5Np6QK6tB+TaekDaV4wBfcVhtweZp4plhsNuEJJlch+Q8Xx8JxDzPL4PCMbpnUAkuweI5oA5fpXbg3zYA/KAdM2ABoawILHfDUEsrIVSYmQlFd4ggRjsGhRCbm7k9tGHcfO2G0NMmnH6bD2sUN9oJDA4JFHAK9TCBEINjhNBgtpgCKLOpGk6f/eZRmM8ZHhIMA0kjxEHYcRjBoyHFxLbsrRuiW1ELkyOF5MqgJ3CaB0vD/GBXiEJknha3x9EqDDGfnnaNP9T6xEbEPsxSog4hlcRweUhMvNewUgSUTs7dOgH4yymTnDQDBIfQvwGEgPoA8KgCugsRlCL/cwUOdE6TyTch0iQBpKoTvuAwEYK5JGBBsEuSyZo2U5uuYPQ6vvWYi8QqAImImzYHw47w39HKrmDQEPHrdU9FRicIBHEZgeRqNlacRxzIIgFc5wWPbih0BAwAjF1Q0Fyj9OjEryKs9aMkC4QQbS8AogVmaFbSWE5PKQsBd2KoynVDAwhWEbdJD6awkzBYSF2RmroIAlPv9ZZUQRRDgkhLEFVnJxdVa5pTchwEFHZcIhO2bVzE/jZYBCCBmdo4lYuhQE5EESnOEP2ya2D+w4wMQNBYNG98MOlKFwBiGAQiCRE98pqUaCGlKizOgQEtOhctqhCc3EWAD0HgFhzljF/sagrXrCJsfQOYSxnGPMJ/sgb9A0RnmfMDxPpGWKUnce8Kj942p4hPNeYH8deIUl8ljE/ThLb84ycdcx3kegR0vl3UOZS/CVOfX8QSI5uBfwtsewPIrhkZG+QWNnmUy/CKWdMr+aq/noLK+fcKuwU6WqClvUFmbY5nVTuKXtxK7xVNqyWEKPSE4QtzhGFqwC7cAGbJounl9YQQQ+QtsP43f0AUG0h1mLSFgKyJ8gsxj+zi0WoNRW2tYdIkl/TsJcLV7kfYR9ybcPuvbQ6TC1WlZucAjGx9AKhsM2MVBZh4YoTICLsB4IWrV0K4MmlJ0DAXiDUhG0gawAvJ0FsYi8FGRvkJVAatK1wbvJ97ZqvrkV6yrCbcV1+bsi8RPEMSXMA9K04aeWcWxRA6lb/CaG3gKaXgBTf8DNTfUYdjUGriqx8u1o02WDbQoxpIJfYWlFmnouynKNOtMngQuXa9L4iZ4dIunzV+WuJumXatMSFmqd12aUgyH63d647TkJRGPW62Bc8UEAKVVutjcYZ5/1fz3aMoU47sQjDZdKV9ld/NCvsDw6cfQ43ay0KA3A7oDT0f0TKpyotNgtntWgq+EkR14PIer3uPexEG4jy5pzypEiSgG/2RBMYNHZjOlf2LsQWT2esdZZpjbUQ/1+ViY1+y6C0ZGvtuhB0QneIYL429h+wHXhON/oX0VBzCbuft5+3t59XsFqxuVvcORehVjGdO0Rgt+a2YLOFHWwLcrv4D7qI9G+yMxYr8i3rLdh2t3UuQoWBRMqgrUS2a1g529WllxEd/3HQYnlG5CdQ7BZ3RsMUwg5Jqpzly/FdoIMfvvkGwHLnIhLTwURcHum9evd+XvMjaMUp2cdPX+mMuzCYiNTOCcvDfHN3JCTjzuouDg962zCFWV3E7OwDue5oWg0pgvQS8wl0PuDWY8wb4iAMLCJ9xrzBfWARpNKTmHemchhaJMS9xny8DjpQJ+s15sTA8CJu+vXT10OTaG8kNoYIYN/e9RdzVGAckTqJPn2mJ8bsxBbkpregj9kbj5j20cjsAlAGGE1EiEXSTOiCRibEgo66osf2Hko3PDJGXtGDSJorXfEPEcKoIlR5TXfiZTn68j1Fu6qooeOvQ4TQLacIiTO8iLiCKKggyj1J1XGt7j3qAqJ7hhDxNJhJbEikf079Uv7/tUQq1DlgVmcxITOzIUQsgdQ1w6OEpm0gdZT2iGAx92gkuBHCQKUVpy4gmYRgRNrUuQfaI812L5IGBUI9UGnhFlVgnkrm2bGhiAmt8AqXRiqkqd6XVjyEiAqSKWWdUlvNMXFF1UIl0b/MRaEcsLQsQTLHo0AcxScDcdQva+RNUOMYzYTECLWIDCEiFmUJSOr7r578Sq1UPKDIT1YkUCv6QC2NTCnTPVPojXc8kCjymIiQqKSowvjzI/9KkTW7OR2LCC5ixBUOTF4EUEr/bVOJ+F5EfGr7a7UgaXY8y8VIHJvQjmftEBxxlhElohwzL5FHuYpcRXrgw3LtnDA/kc3N+kfBCfMTKSJ0ozxkfiIfio1xyvxEKDfFkhNmJ6K54gvnIbMTocht+f05ZMSXq3MhmZ/IWa4iV5G5cBWZGleRqXEVmRpXkalxFZkaz0jkxfN4QXD04lXFMyB+u3/7dDz7YyLVXuP1yzfR3Hnx6uXrXzaOLHMeGQCLAAAAAElFTkSuQmCC",
"description": "Displays the latest values of the wind speed and direction.",
"descriptor": {
"type": "latest",
"sizeX": 3,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-wind-speed-direction-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-wind-speed-direction-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.windSpeedDirectionWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 2,\n singleEntity: true,\n previewWidth: '270px',\n previewHeight: '270px',\n embedTitlePanel: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'cardClick': {\n name: 'widget-action.card-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-wind-speed-direction-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-wind-speed-direction-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"windDirection\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 360;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 360) {\\n\\tvalue = 360;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"centerValue\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 30) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":\"m/s\",\"decimals\":1,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"centerValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"centerValueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"ticksColor\":\"rgba(0, 0, 0, 0.12)\",\"directionalNamesElseDegrees\":true,\"majorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"majorTicksColor\":\"rgba(158, 158, 158, 1)\",\"minorTicksFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"minorTicksColor\":\"rgba(0, 0, 0, 0.12)\",\"arrowColor\":\"rgba(0, 0, 0, 0.87)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Wind speed and direction\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"air\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
},
"externalId": null,
"tags": [
"wind",
"weather",
"compass",
"degrees"
]
}

6
application/src/main/data/upgrade/3.6.0/schema_update.sql

@ -19,3 +19,9 @@ ALTER TABLE widget_type
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS tbel_exec varchar(32);
UPDATE api_usage_state SET tbel_exec = js_exec WHERE tbel_exec IS NULL;
ALTER TABLE notification DROP CONSTRAINT IF EXISTS fk_notification_request_id;
ALTER TABLE notification DROP CONSTRAINT IF EXISTS fk_notification_recipient_id;
CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notification(request_id);
CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id ON notification_request(tenant_id);

2
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java

@ -74,7 +74,7 @@ public class DefaultActorService extends TbApplicationEventListener<PartitionCha
@Value("${actors.system.device_dispatcher_pool_size:4}")
private int deviceDispatcherSize;
@Value("${actors.system.rule_dispatcher_pool_size:4}")
@Value("${actors.system.rule_dispatcher_pool_size:8}")
private int ruleDispatcherSize;
@PostConstruct

12
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -286,7 +286,7 @@ public class AlarmController extends BaseController {
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)));
}
@ApiOperation(value = "Get All Alarms (getAllAlarms)",
@ -335,9 +335,9 @@ public class AlarmController extends BaseController {
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)));
} else {
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)));
}
}
@ -402,7 +402,7 @@ public class AlarmController extends BaseController {
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)));
}
@ApiOperation(value = "Get All Alarms (getAllAlarmsV2)",
@ -461,9 +461,9 @@ public class AlarmController extends BaseController {
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)));
} else {
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)));
}
}

2
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -153,7 +153,7 @@ public class AssetController extends BaseController {
checkParameter(ASSET_ID, strAssetId);
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId, Operation.DELETE);
tbAssetService.delete(asset, getCurrentUser()).get();
tbAssetService.delete(asset, getCurrentUser());
}
@ApiOperation(value = "Assign asset to customer (assignAssetToCustomer)",

2
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -228,7 +228,7 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId, Operation.DELETE);
tbDeviceService.delete(device, getCurrentUser()).get();
tbDeviceService.delete(device, getCurrentUser());
}
@ApiOperation(value = "Assign device to customer (assignDeviceToCustomer)",

2
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -151,6 +151,8 @@ public class TenantProfileController extends BaseController {
" \"defaultStorageTtlDays\": 0,\n" +
" \"alarmsTtlDays\": 0,\n" +
" \"rpcTtlDays\": 0,\n" +
" \"queueStatsTtlDays\": 0,\n" +
" \"ruleEngineExceptionsTtlDays\": 0,\n" +
" \"warnThreshold\": 0\n" +
" }\n" +
" },\n" +

20
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
@ -46,7 +45,6 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
public abstract class AbstractTbEntityService {
@ -63,26 +61,22 @@ public abstract class AbstractTbEntityService {
protected EdgeService edgeService;
@Autowired
protected AlarmService alarmService;
@Autowired @Lazy
@Autowired
@Lazy
protected AlarmSubscriptionService alarmSubscriptionService;
@Autowired
protected CustomerService customerService;
@Autowired
protected TbClusterService tbClusterService;
@Autowired(required = false) @Lazy
@Autowired(required = false)
@Lazy
private EntitiesVersionControlService vcService;
protected ListenableFuture<Void> removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
ListenableFuture<PageData<AlarmInfo>> alarmsFuture =
protected void removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
PageData<AlarmInfo> alarms =
alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false));
ListenableFuture<List<AlarmId>> alarmIdsFuture = Futures.transform(alarmsFuture, page ->
page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor);
return Futures.transform(alarmIdsFuture, ids -> {
ids.stream().map(alarmId -> alarmService.deleteAlarm(tenantId, alarmId)).collect(Collectors.toList());
return null;
}, dbExecutor);
alarms.getData().stream().map(AlarmInfo::getId).forEach(alarmId -> alarmService.delAlarm(tenantId, alarmId));
}
protected <T> T checkNotNull(T reference) throws ThingsboardException {

7
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -15,9 +15,9 @@
*/
package org.thingsboard.server.service.entitiy.asset;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -69,14 +69,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
}
@Override
public ListenableFuture<Void> delete(Asset asset, User user) {
@Transactional
public void delete(Asset asset, User user) {
ActionType actionType = ActionType.DELETED;
TenantId tenantId = asset.getTenantId();
AssetId assetId = asset.getId();
try {
removeAlarmsByEntityId(tenantId, assetId);
assetService.deleteAsset(tenantId, assetId);
logEntityActionService.logEntityAction(tenantId, assetId, asset, asset.getCustomerId(), actionType, user, assetId.toString());
return removeAlarmsByEntityId(tenantId, assetId);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.ASSET), actionType, user, e,
assetId.toString());

3
application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.entitiy.asset;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
@ -28,7 +27,7 @@ public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception;
ListenableFuture<Void> delete(Asset asset, User user);
void delete(Asset asset, User user);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException;

7
application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
@ -87,16 +88,16 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
}
@Override
public ListenableFuture<Void> delete(Device device, User user) {
@Transactional
public void delete(Device device, User user) {
ActionType actionType = ActionType.DELETED;
TenantId tenantId = device.getTenantId();
DeviceId deviceId = device.getId();
try {
removeAlarmsByEntityId(tenantId, deviceId);
deviceService.deleteDevice(tenantId, deviceId);
logEntityActionService.logEntityAction(tenantId, deviceId, device, device.getCustomerId(), actionType,
user, deviceId.toString());
return removeAlarmsByEntityId(tenantId, deviceId);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), actionType,
user, e, deviceId.toString());

2
application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java

@ -35,7 +35,7 @@ public interface TbDeviceService {
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException;
ListenableFuture<Void> delete(Device device, User user);
void delete(Device device, User user);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException;

1
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -532,6 +532,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
this.deleteSystemWidgetBundle("html_widgets");
this.deleteSystemWidgetBundle("tables");
this.deleteSystemWidgetBundle("count_widgets");
this.deleteSystemWidgetBundle("weather_widgets");
installScripts.loadSystemWidgets();
}

30
application/src/main/java/org/thingsboard/server/service/stats/DefaultRuleEngineStatisticsService.java

@ -17,7 +17,9 @@ package org.thingsboard.server.service.stats;
import com.google.common.util.concurrent.FutureCallback;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
@ -26,7 +28,9 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats;
@ -37,6 +41,7 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@ -44,9 +49,11 @@ import java.util.stream.Collectors;
@TbRuleEngineComponent
@Service
@Slf4j
@RequiredArgsConstructor
public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsService {
public static final String TB_SERVICE_QUEUE = "TbServiceQueue";
public static final String RULE_ENGINE_EXCEPTION = "ruleEngineException";
public static final FutureCallback<Integer> CALLBACK = new FutureCallback<Integer>() {
@Override
public void onSuccess(@Nullable Integer result) {
@ -61,16 +68,13 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS
private final TbServiceInfoProvider serviceInfoProvider;
private final TelemetrySubscriptionService tsService;
private final Lock lock = new ReentrantLock();
private final AssetService assetService;
private final ConcurrentMap<TenantQueueKey, AssetId> tenantQueueAssets;
private final ApiLimitService apiLimitService;
private final Lock lock = new ReentrantLock();
private final ConcurrentMap<TenantQueueKey, AssetId> tenantQueueAssets = new ConcurrentHashMap<>();
public DefaultRuleEngineStatisticsService(TelemetrySubscriptionService tsService, TbServiceInfoProvider serviceInfoProvider, AssetService assetService) {
this.tsService = tsService;
this.serviceInfoProvider = serviceInfoProvider;
this.assetService = assetService;
this.tenantQueueAssets = new ConcurrentHashMap<>();
}
@Value("${queue.rule-engine.stats.max-error-message-length:4096}")
private int maxErrorMessageLength;
@Override
public void reportQueueStats(long ts, TbRuleEngineConsumerStats ruleEngineStats) {
@ -84,7 +88,9 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS
.map(kv -> new BasicTsKvEntry(ts, new LongDataEntry(kv.getKey(), (long) kv.getValue().get())))
.collect(Collectors.toList());
if (!tsList.isEmpty()) {
tsService.saveAndNotifyInternal(tenantId, serviceAssetId, tsList, CALLBACK);
long ttl = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getQueueStatsTtlDays);
ttl = TimeUnit.DAYS.toSeconds(ttl);
tsService.saveAndNotifyInternal(tenantId, serviceAssetId, tsList, ttl, CALLBACK);
}
}
} catch (Exception e) {
@ -95,8 +101,10 @@ public class DefaultRuleEngineStatisticsService implements RuleEngineStatisticsS
});
ruleEngineStats.getTenantExceptions().forEach((tenantId, e) -> {
try {
TsKvEntry tsKv = new BasicTsKvEntry(e.getTs(), new JsonDataEntry("ruleEngineException", e.toJsonString()));
tsService.saveAndNotifyInternal(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), CALLBACK);
TsKvEntry tsKv = new BasicTsKvEntry(e.getTs(), new JsonDataEntry(RULE_ENGINE_EXCEPTION, e.toJsonString(maxErrorMessageLength)));
long ttl = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getRuleEngineExceptionsTtlDays);
ttl = TimeUnit.DAYS.toSeconds(ttl);
tsService.saveAndNotifyInternal(tenantId, getServiceAssetId(tenantId, queueName), Collections.singletonList(tsKv), ttl, CALLBACK);
} catch (Exception e2) {
if (!"Asset is referencing to non-existent tenant!".equalsIgnoreCase(e2.getMessage())) {
log.debug("[{}] Failed to store the statistics", tenantId, e2);

10
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ProtocolStringList;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -93,6 +94,11 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private final SmsService smsService;
private volatile ScheduledExecutorService scheduler;
@Value("${metrics.system_info.persist_frequency:60}")
private int systemInfoPersistFrequencySeconds;
@Value("#{${metrics.system_info.ttl:7} * 86400}")
private int systemInfoTtlSeconds;
@Override
protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
@ -101,7 +107,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
if (myPartition) {
if (scheduler == null) {
scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-system-info-scheduler"));
scheduler.scheduleAtFixedRate(this::saveCurrentSystemInfo, 0, 1, TimeUnit.MINUTES);
scheduler.scheduleWithFixedDelay(this::saveCurrentSystemInfo, 0, systemInfoPersistFrequencySeconds, TimeUnit.SECONDS);
}
} else {
destroy();
@ -195,7 +201,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private void doSave(List<TsKvEntry> telemetry) {
ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID);
telemetryService.saveAndNotifyInternal(TenantId.SYS_TENANT_ID, apiUsageState.getId(), telemetry, CALLBACK);
telemetryService.saveAndNotifyInternal(TenantId.SYS_TENANT_ID, apiUsageState.getId(), telemetry, systemInfoTtlSeconds, CALLBACK);
}
private List<SystemInfoData> getSystemData(ServiceInfo serviceInfo) {

80
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -53,7 +53,6 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
@ -115,31 +114,6 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs));
}
@Override
public Alarm createOrUpdateAlarm(Alarm alarm) {
AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled());
if (result.isSuccessful()) {
onAlarmUpdated(result);
AlarmSeverity oldSeverity = result.getOldSeverity();
if (oldSeverity != null && !oldSeverity.equals(result.getAlarm().getSeverity())) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm severity was updated from %s to %s", oldSeverity, result.getAlarm().getSeverity())))
.build();
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, null);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
}
}
if (result.isCreated()) {
apiUsageClient.report(alarm.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT);
}
return result.getAlarm();
}
@Override
public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) {
AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId);
@ -147,26 +121,6 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return result.isSuccessful();
}
@Override
public ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) {
ListenableFuture<AlarmApiCallResult> result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs));
Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor);
return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor);
}
@Override
public ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) {
AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details);
return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor);
}
@Override
public ListenableFuture<AlarmOperationResult> clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) {
AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details);
Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor);
return Futures.immediateFuture(new AlarmOperationResult(result));
}
@Override
public ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) {
return alarmService.findAlarmByIdAsync(tenantId, alarmId);
@ -183,22 +137,22 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query) {
public PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query) {
return alarmService.findAlarms(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
return alarmService.findCustomerAlarms(tenantId, customerId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
public PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmService.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
public PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmService.findCustomerAlarmsV2(tenantId, customerId, query);
}
@ -218,23 +172,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}
@Override
public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestByOriginatorAndType(tenantId, originator, type);
}
@Deprecated
private void onAlarmUpdated(AlarmOperationResult result) {
wsCallBackExecutor.submit(() -> {
AlarmInfo alarm = new AlarmInfo(result.getAlarm());
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
}, () -> {
return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
});
}
});
public Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type);
}
@Override
@ -248,10 +187,9 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
}, () -> {
return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
});
subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
}, () -> TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm)
);
}
notificationRuleProcessor.process(AlarmTrigger.builder()
.tenantId(tenantId)

1
application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java

@ -92,7 +92,6 @@ public class AlarmsCleanUpService {
while (true) {
PageData<AlarmId> toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest);
for (AlarmId alarmId : toRemove.getData()) {
relationService.deleteEntityRelations(tenantId, alarmId);
Alarm alarm = alarmService.delAlarm(tenantId, alarmId, false).getAlarm();
if (alarm != null) {
entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null);

4
application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java

@ -63,8 +63,8 @@ public class NotificationsCleanUpService extends AbstractCleanUpService {
if (lastRemovedNotificationTs > 0) {
long gap = TimeUnit.MINUTES.toMillis(10);
long requestExpTime = lastRemovedNotificationTs - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap;
// TODO: double-check this
notificationRequestDao.removeAllByCreatedTimeBefore(requestExpTime);
int removed = notificationRequestDao.removeAllByCreatedTimeBefore(requestExpTime);
log.info("Removed {} outdated notification requests older than {}", removed, requestExpTime);
}
}

648
application/src/main/resources/thingsboard.yml

File diff suppressed because it is too large

65
application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java

@ -41,6 +41,8 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmDao;
@ -48,6 +50,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@ -806,10 +809,14 @@ public class AlarmControllerTest extends AbstractControllerTest {
}
private AlarmInfo createAlarm(String type) throws Exception {
return createAlarm(type, customerDevice.getId(), customerId);
}
private AlarmInfo createAlarm(String type, EntityId originatorId, CustomerId customerId) throws Exception {
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.customerId(customerId)
.originator(customerDevice.getId())
.originator(originatorId)
.severity(AlarmSeverity.CRITICAL)
.type(type)
.build();
@ -963,4 +970,60 @@ public class AlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundTypes.isEmpty());
}
@Test
public void testDeleteAlarmTypesAfterDeletingAlarmOriginator() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
List<String> types = new ArrayList<>();
for (int i = 0; i < 13; i++) {
Device device = new Device();
device.setName("Test_device_" + i);
devices.add(device = doPost("/api/device", device, Device.class));
types.add(createAlarm(TEST_ALARM_TYPE + i, device.getId(), null).getType());
}
devices.sort(Comparator.comparing(Device::getName));
Collections.sort(types);
List<String> foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference<PageData<EntitySubtype>>() {
})
.getData()
.stream()
.map(EntitySubtype::getType)
.sorted()
.collect(Collectors.toList());
Assert.assertEquals(13, foundTypes.size());
Assert.assertEquals(types, foundTypes);
for (int i = 0; i < 12; i++) {
doDelete("/api/device/" + devices.remove(0).getId()).andExpect(status().isOk());
types.remove(0);
}
foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference<PageData<EntitySubtype>>() {
})
.getData()
.stream()
.map(EntitySubtype::getType)
.collect(Collectors.toList());
Assert.assertEquals(types.size(), foundTypes.size());
Assert.assertEquals(types, foundTypes);
doDelete("/api/device/" + devices.get(0).getId()).andExpect(status().isOk());
foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference<PageData<EntitySubtype>>() {
})
.getData()
.stream()
.map(EntitySubtype::getType)
.sorted()
.collect(Collectors.toList());
Assert.assertTrue(foundTypes.isEmpty());
}
}

36
application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java

@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
@ -291,6 +294,39 @@ public class AssetControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Asset", assetIdStr))));
}
@Test
public void testDeleteAssetWithAlarmsAndAlarmTypes() throws Exception {
Asset asset = new Asset();
asset.setName("My asset");
asset.setType("default");
Asset savedAsset = doPost("/api/asset", asset, Asset.class);
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.originator(savedAsset.getId())
.severity(AlarmSeverity.CRITICAL)
.type("test_type")
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
doDelete("/api/asset/" + savedAsset.getId().getId().toString())
.andExpect(status().isOk());
String assetIdStr = savedAsset.getId().getId().toString();
doGet("/api/asset/" + assetIdStr)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Asset", assetIdStr))));
doGet("/api/alarm/info/" + alarm.getId())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Alarm", alarm.getId().getId().toString()))));
}
@Test
public void testDeleteAssetAssignedToEntityView() throws Exception {
Asset asset1 = new Asset();

140
application/src/test/java/org/thingsboard/server/controller/BaseQueueControllerTest.java

@ -16,9 +16,19 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
@ -26,13 +36,51 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
import org.thingsboard.server.service.stats.DefaultRuleEngineStatisticsService;
import org.thingsboard.server.service.stats.RuleEngineStatisticsService;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
@DaoSqlTest
@TestPropertySource(properties = {
"queue.rule-engine.stats.max-error-message-length=100"
})
public class BaseQueueControllerTest extends AbstractControllerTest {
@Autowired
private RuleEngineStatisticsService ruleEngineStatisticsService;
@Autowired
private StatsFactory statsFactory;
@SpyBean
private TimeseriesDao timeseriesDao;
@Autowired
private AssetService assetService;
@Test
public void testQueueWithServiceTypeRE() throws Exception {
loginSysAdmin();
@ -93,4 +141,96 @@ public class BaseQueueControllerTest extends AbstractControllerTest {
.andExpect(status().isOk());
}
@Test
public void testQueueStatsTtl() throws ThingsboardException {
Queue queue = new Queue();
queue.setName("Test-1");
queue.setTenantId(TenantId.SYS_TENANT_ID);
TbRuleEngineProcessingResult testProcessingResult = Mockito.mock(TbRuleEngineProcessingResult.class);
TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg> msg = new TbProtoQueueMsg<>(UUID.randomUUID(),
TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.build());
when(testProcessingResult.getSuccessMap()).thenReturn(Stream.generate(() -> msg)
.limit(5).collect(Collectors.toConcurrentMap(m -> UUID.randomUUID(), m -> m)));
when(testProcessingResult.getFailedMap()).thenReturn(Stream.generate(() -> msg)
.limit(5).collect(Collectors.toConcurrentMap(m -> UUID.randomUUID(), m -> m)));
when(testProcessingResult.getPendingMap()).thenReturn(new ConcurrentHashMap<>());
RuleEngineException ruleEngineException = new RuleEngineException("Test Exception");
when(testProcessingResult.getExceptionsMap()).thenReturn(new ConcurrentHashMap<>(Map.of(
tenantId, ruleEngineException
)));
TbRuleEngineConsumerStats testStats = new TbRuleEngineConsumerStats(queue, statsFactory);
testStats.log(testProcessingResult, true);
int queueStatsTtlDays = 14;
int ruleEngineExceptionsTtlDays = 7;
updateDefaultTenantProfileConfig(profileConfiguration -> {
profileConfiguration.setQueueStatsTtlDays(queueStatsTtlDays);
profileConfiguration.setRuleEngineExceptionsTtlDays(ruleEngineExceptionsTtlDays);
});
ruleEngineStatisticsService.reportQueueStats(System.currentTimeMillis(), testStats);
Asset serviceAsset = assetService.findAssetsByTenantIdAndType(tenantId, TB_SERVICE_QUEUE, new PageLink(100)).getData()
.stream().filter(asset -> asset.getName().startsWith(queue.getName()))
.findFirst().get();
ArgumentCaptor<Long> ttlCaptor = ArgumentCaptor.forClass(Long.class);
verify(timeseriesDao).save(eq(tenantId), eq(serviceAsset.getId()), argThat(tsKvEntry -> {
return tsKvEntry.getKey().equals(TbRuleEngineConsumerStats.SUCCESSFUL_MSGS) &&
tsKvEntry.getLongValue().get().equals(5L);
}), ttlCaptor.capture());
verify(timeseriesDao).save(eq(tenantId), eq(serviceAsset.getId()), argThat(tsKvEntry -> {
return tsKvEntry.getKey().equals(TbRuleEngineConsumerStats.FAILED_MSGS) &&
tsKvEntry.getLongValue().get().equals(5L);
}), ttlCaptor.capture());
assertThat(ttlCaptor.getAllValues()).allSatisfy(usedTtl -> {
assertThat(usedTtl).isEqualTo(TimeUnit.DAYS.toSeconds(queueStatsTtlDays));
});
verify(timeseriesDao).save(eq(tenantId), eq(serviceAsset.getId()), argThat(tsKvEntry -> {
return tsKvEntry.getKey().equals(DefaultRuleEngineStatisticsService.RULE_ENGINE_EXCEPTION) &&
tsKvEntry.getJsonValue().get().equals(ruleEngineException.toJsonString(0));
}), ttlCaptor.capture());
assertThat(ttlCaptor.getValue()).isEqualTo(TimeUnit.DAYS.toSeconds(ruleEngineExceptionsTtlDays));
}
@Test
public void testRuleEngineExceptionTruncation() {
Queue queue = new Queue();
queue.setName("Test-2");
queue.setTenantId(TenantId.SYS_TENANT_ID);
TbRuleEngineProcessingResult testProcessingResult = Mockito.mock(TbRuleEngineProcessingResult.class);
when(testProcessingResult.getSuccessMap()).thenReturn(new ConcurrentHashMap<>());
when(testProcessingResult.getFailedMap()).thenReturn(new ConcurrentHashMap<>());
when(testProcessingResult.getPendingMap()).thenReturn(new ConcurrentHashMap<>());
String largeExceptionMessage = RandomStringUtils.randomAlphabetic(150);
RuleEngineException ruleEngineException = new RuleEngineException(largeExceptionMessage);
when(testProcessingResult.getExceptionsMap()).thenReturn(new ConcurrentHashMap<>(Map.of(
tenantId, ruleEngineException
)));
TbRuleEngineConsumerStats testStats = new TbRuleEngineConsumerStats(queue, statsFactory);
testStats.log(testProcessingResult, true);
ruleEngineStatisticsService.reportQueueStats(System.currentTimeMillis(), testStats);
AtomicReference<TsKvEntry> reExceptionTsKvEntryCaptor = new AtomicReference<>();
verify(timeseriesDao).save(eq(tenantId), any(), argThat(tsKvEntry -> {
if (tsKvEntry.getKey().equals(DefaultRuleEngineStatisticsService.RULE_ENGINE_EXCEPTION)) {
reExceptionTsKvEntryCaptor.set(tsKvEntry);
return true;
}
return false;
}), anyLong());
TsKvEntry reExceptionTsKvEntry = reExceptionTsKvEntryCaptor.get();
String finalErrorMessage = JacksonUtil.toJsonNode(reExceptionTsKvEntry.getJsonValue().get()).get("message").asText();
assertThat(finalErrorMessage).isEqualTo(largeExceptionMessage.substring(0, 100) + "...[truncated 50 symbols]");
}
}

37
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -48,6 +48,10 @@ import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
@ -504,6 +508,39 @@ public class DeviceControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Device", savedDeviceId.getId().toString()))));
}
@Test
public void testDeleteDeviceWithAlarmsAndAlarmTypes() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.originator(savedDevice.getId())
.severity(AlarmSeverity.CRITICAL)
.type("test_type")
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
doDelete("/api/device/" + savedDevice.getId().getId().toString())
.andExpect(status().isOk());
String DeviceIdStr = savedDevice.getId().getId().toString();
doGet("/api/device/" + DeviceIdStr)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Device", DeviceIdStr))));
doGet("/api/device/info/" + alarm.getId())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Device", alarm.getId().getId().toString()))));
}
@Test
public void testSaveDeviceWithEmptyType() throws Exception {
Device device = new Device();

4
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -61,6 +61,7 @@ import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import java.net.URISyntaxException;
import java.util.Arrays;
@ -90,6 +91,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected NotificationTargetService notificationTargetService;
@Autowired
protected NotificationRequestService notificationRequestService;
@Autowired
protected SqlPartitioningRepository partitioningRepository;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@ -100,6 +103,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID);
notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
partitioningRepository.dropPartitionsBefore("notification", Long.MAX_VALUE, 1);
}
protected NotificationTarget createNotificationTarget(UserId... usersIds) {

28
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -25,9 +25,11 @@ import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
@ -62,6 +64,7 @@ import org.thingsboard.server.common.data.notification.template.NotificationTemp
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationDao;
@ -74,6 +77,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -279,6 +283,25 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(getMyNotifications(false, 10)).size().isZero();
}
@Test
public void whenTenantIsDeleted_thenDeleteNotificationRequests() throws Exception {
createDifferentTenant();
TenantId tenantId = differentTenantId;
NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId());
int notificationsCount = 20;
for (int i = 0; i < notificationsCount; i++) {
NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB);
awaitNotificationRequest(request.getId());
}
List<NotificationRequest> requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData();
assertThat(requests).size().isEqualTo(notificationsCount);
deleteDifferentTenant();
assertThat(notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(1)).getTotalElements())
.isZero();
}
@Test
public void testNotificationUpdatesForSeveralUsers() throws Exception {
int usersCount = 150;
@ -692,6 +715,11 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
return future.get(30, TimeUnit.SECONDS);
}
private NotificationRequestStats awaitNotificationRequest(NotificationRequestId requestId) {
return await().atMost(30, TimeUnit.SECONDS)
.until(() -> getStats(requestId), Objects::nonNull);
}
private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) {
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications);
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE);

8
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -230,8 +230,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType);
long ts = System.currentTimeMillis();
await().atMost(15, TimeUnit.SECONDS)
@ -313,8 +313,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType);
getWsClient().waitForUpdate(true);
Notification notification = getWsClient().getLastDataUpdate().getUpdate();

64
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java

@ -1,64 +0,0 @@
/**
* Copyright © 2016-2023 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.server.dao.alarm;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections;
import java.util.List;
@Builder
@Data
@AllArgsConstructor
@Deprecated
public class AlarmOperationResult {
private final Alarm alarm;
private final boolean successful;
private final boolean created;
private final AlarmSeverity oldSeverity;
private final List<EntityId> propagatedEntitiesList;
public AlarmOperationResult(Alarm alarm, boolean successful) {
this(alarm, successful, Collections.emptyList());
}
public AlarmOperationResult(Alarm alarm, boolean successful, List<EntityId> propagatedEntitiesList) {
this(alarm, successful, false, null, propagatedEntitiesList);
}
public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List<EntityId> propagatedEntitiesList) {
this.alarm = alarm;
this.successful = successful;
this.created = created;
this.propagatedEntitiesList = propagatedEntitiesList;
this.oldSeverity = null;
}
//Temporary while we have not removed the AlarmOperationResult.
public AlarmOperationResult(AlarmApiCallResult result) {
this.alarm = result.getAlarm() != null ? new Alarm(result.getAlarm()) : null;
this.successful = result.isSuccessful() && (result.isCreated() || result.isModified());
this.created = result.isCreated();
this.oldSeverity = result.getOldSeverity();
this.propagatedEntitiesList = result.getPropagatedEntitiesList();
}
}

31
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -81,27 +81,6 @@ public interface AlarmService extends EntityDaoService {
void delAlarmTypes(TenantId tenantId, Set<String> types);
/*
* Legacy API, before 3.5.
*/
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult createOrUpdateAlarm(Alarm alarm);
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<AlarmOperationResult> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
// Other API
Alarm findAlarmById(TenantId tenantId, AlarmId alarmId);
@ -109,13 +88,13 @@ public interface AlarmService extends EntityDaoService {
AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId);
ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query);
PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId);
@ -129,6 +108,8 @@ public interface AlarmService extends EntityDaoService {
void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId);
void deleteEntityAlarmRecordsByTenantId(TenantId tenantId);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query);
PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java

@ -36,4 +36,6 @@ public interface DeviceCredentialsService {
void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
void deleteDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId);
}

1
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;

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

@ -76,6 +76,8 @@ public interface EntityViewService extends EntityDaoService {
List<EntityView> findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
boolean existsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
void deleteEntityView(TenantId tenantId, EntityViewId entityViewId);
void deleteEntityViewsByTenantId(TenantId tenantId);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiLimitService.java

@ -17,9 +17,14 @@ package org.thingsboard.server.dao.usagerecord;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.function.Function;
public interface ApiLimitService {
boolean checkEntitiesLimit(TenantId tenantId, EntityType entityType);
long getLimit(TenantId tenantId, Function<DefaultTenantProfileConfiguration, Number> extractor);
}

69
common/data/src/main/java/org/thingsboard/server/common/data/EntitySubtype.java

@ -15,71 +15,19 @@
*/
package org.thingsboard.server.common.data;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
public class EntitySubtype {
import java.io.Serializable;
private static final long serialVersionUID = 8057240243059922101L;
private TenantId tenantId;
private EntityType entityType;
private String type;
public EntitySubtype() {
super();
}
public EntitySubtype(TenantId tenantId, EntityType entityType, String type) {
this.tenantId = tenantId;
this.entityType = entityType;
this.type = type;
}
public TenantId getTenantId() {
return tenantId;
}
public void setTenantId(TenantId tenantId) {
this.tenantId = tenantId;
}
public EntityType getEntityType() {
return entityType;
}
public void setEntityType(EntityType entityType) {
this.entityType = entityType;
}
@Data
public class EntitySubtype implements Serializable {
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EntitySubtype that = (EntitySubtype) o;
if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
if (entityType != that.entityType) return false;
return type != null ? type.equals(that.type) : that.type == null;
private static final long serialVersionUID = 8057240243059922101L;
}
@Override
public int hashCode() {
int result = tenantId != null ? tenantId.hashCode() : 0;
result = 31 * result + (entityType != null ? entityType.hashCode() : 0);
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
private final TenantId tenantId;
private final EntityType entityType;
private final String type;
@Override
public String toString() {
@ -90,5 +38,4 @@ public class EntitySubtype {
sb.append('}');
return sb.toString();
}
}

13
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -20,6 +20,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.function.Function;
import static org.apache.commons.lang3.StringUtils.repeat;
@ -228,4 +229,16 @@ public class StringUtils {
return generateSafeToken(DEFAULT_TOKEN_LENGTH);
}
public static String truncate(String string, int maxLength) {
return truncate(string, maxLength, n -> "...[truncated " + n + " symbols]");
}
public static String truncate(String string, int maxLength, Function<Integer, String> truncationMarkerFunc) {
if (string == null || maxLength <= 0 || string.length() <= maxLength) {
return string;
}
int truncatedSymbols = string.length() - maxLength;
return string.substring(0, maxLength) + truncationMarkerFunc.apply(truncatedSymbols);
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Collections;
@ -27,6 +28,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
@ApiModel
@EqualsAndHashCode
public class PageData<T> implements Serializable {
public static final PageData EMPTY_PAGE_DATA = new PageData<>();

2
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -83,6 +83,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private int defaultStorageTtlDays;
private int alarmsTtlDays;
private int rpcTtlDays;
private int queueStatsTtlDays;
private int ruleEngineExceptionsTtlDays;
private double warnThreshold;

11
common/data/src/test/java/org/thingsboard/server/common/data/StringUtilsTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ -37,4 +38,14 @@ class StringUtilsTest {
assertThat(StringUtils.contains0x00(sample)).isFalse();
}
@Test
void testTruncate() {
int maxLength = 5;
assertThat(StringUtils.truncate(null, maxLength)).isNull();
assertThat(StringUtils.truncate("", maxLength)).isEmpty();
assertThat(StringUtils.truncate("123", maxLength)).isEqualTo("123");
assertThat(StringUtils.truncate("1234567", maxLength)).isEqualTo("12345...[truncated 2 symbols]");
assertThat(StringUtils.truncate("1234567", 0)).isEqualTo("1234567");
}
}

16
common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleEngineException.java

@ -19,17 +19,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.StringUtils;
@Slf4j
public class RuleEngineException extends Exception {
protected static final ObjectMapper mapper = new ObjectMapper();
@Getter
private long ts;
private final long ts;
public RuleEngineException(String message) {
super(message != null ? message : "Unknown");
ts = System.currentTimeMillis();
this(message, null);
}
public RuleEngineException(String message, Throwable t) {
@ -37,12 +37,18 @@ public class RuleEngineException extends Exception {
ts = System.currentTimeMillis();
}
public String toJsonString() {
public String toJsonString(int maxMessageLength) {
try {
return mapper.writeValueAsString(mapper.createObjectNode().put("message", getMessage()));
return mapper.writeValueAsString(mapper.createObjectNode()
.put("message", truncateIfNecessary(getMessage(), maxMessageLength)));
} catch (JsonProcessingException e) {
log.warn("Failed to serialize exception ", e);
throw new RuntimeException(e);
}
}
protected String truncateIfNecessary(String message, int maxMessageLength) {
return StringUtils.truncate(message, maxMessageLength);
}
}

4
common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java

@ -52,14 +52,14 @@ public class RuleNodeException extends RuleEngineException {
}
}
public String toJsonString() {
public String toJsonString(int maxMessageLength) {
try {
return mapper.writeValueAsString(mapper.createObjectNode()
.put("ruleNodeId", ruleNodeId.toString())
.put("ruleChainId", ruleChainId.toString())
.put("ruleNodeName", ruleNodeName)
.put("ruleChainName", ruleChainName)
.put("message", getMessage()));
.put("message", truncateIfNecessary(getMessage(), maxMessageLength)));
} catch (JsonProcessingException e) {
log.warn("Failed to serialize exception ", e);
throw new RuntimeException(e);

27
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -70,8 +70,7 @@ public class JacksonUtil {
try {
return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueType) : null;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("The given object value: "
+ fromValue + " cannot be converted to " + toValueType, e);
throw new IllegalArgumentException("The given object value cannot be converted to " + toValueType + ": " + fromValue, e);
}
}
@ -79,8 +78,7 @@ public class JacksonUtil {
try {
return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueTypeRef) : null;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("The given object value: "
+ fromValue + " cannot be converted to " + toValueTypeRef, e);
throw new IllegalArgumentException("The given object value cannot be converted to " + toValueTypeRef + ": " + fromValue, e);
}
}
@ -88,8 +86,7 @@ public class JacksonUtil {
try {
return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null;
} catch (IOException e) {
throw new IllegalArgumentException("The given string value: "
+ string + " cannot be transformed to Json object", e);
throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + string, e);
}
}
@ -97,8 +94,7 @@ public class JacksonUtil {
try {
return string != null ? OBJECT_MAPPER.readValue(string, valueTypeRef) : null;
} catch (IOException e) {
throw new IllegalArgumentException("The given string value: "
+ string + " cannot be transformed to Json object", e);
throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + string, e);
}
}
@ -106,8 +102,7 @@ public class JacksonUtil {
try {
return string != null ? OBJECT_MAPPER.readValue(string, javaType) : null;
} catch (IOException e) {
throw new IllegalArgumentException("The given String value: "
+ string + " cannot be transformed to Json object", e);
throw new IllegalArgumentException("The given String value cannot be transformed to Json object: " + string, e);
}
}
@ -115,8 +110,7 @@ public class JacksonUtil {
try {
return bytes != null ? OBJECT_MAPPER.readValue(bytes, clazz) : null;
} catch (IOException e) {
throw new IllegalArgumentException("The given string value: "
+ Arrays.toString(bytes) + " cannot be transformed to Json object", e);
throw new IllegalArgumentException("The given string value cannot be transformed to Json object: " + Arrays.toString(bytes), e);
}
}
@ -124,8 +118,7 @@ public class JacksonUtil {
try {
return OBJECT_MAPPER.readTree(bytes);
} catch (IOException e) {
throw new IllegalArgumentException("The given byte[] value: "
+ Arrays.toString(bytes) + " cannot be transformed to Json object", e);
throw new IllegalArgumentException("The given byte[] value cannot be transformed to Json object: " + Arrays.toString(bytes), e);
}
}
@ -133,8 +126,7 @@ public class JacksonUtil {
try {
return value != null ? OBJECT_MAPPER.writeValueAsString(value) : null;
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("The given Json object value: "
+ value + " cannot be transformed to a String", e);
throw new IllegalArgumentException("The given Json object value cannot be transformed to a String: " + value, e);
}
}
@ -208,8 +200,7 @@ public class JacksonUtil {
try {
return OBJECT_MAPPER.writeValueAsBytes(value);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("The given Json object value: "
+ value + " cannot be transformed to a String", e);
throw new IllegalArgumentException("The given Json object value cannot be transformed to a String: " + value, e);
}
}

2
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -86,6 +86,8 @@ public interface AlarmDao extends Dao<Alarm> {
void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId);
void deleteEntityAlarmRecordsByTenantId(TenantId tenantId);
AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled);
AlarmApiCallResult updateAlarm(AlarmUpdateRequest request);

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

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -162,51 +161,11 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
return result;
}
@Override
public AlarmOperationResult createOrUpdateAlarm(Alarm alarm) {
return createOrUpdateAlarm(alarm, true);
}
@Override
public AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled) {
alarmDataValidator.validate(alarm, Alarm::getTenantId);
try {
if (alarm.getStartTs() == 0L) {
alarm.setStartTs(System.currentTimeMillis());
}
if (alarm.getEndTs() == 0L) {
alarm.setEndTs(alarm.getStartTs());
}
alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator()).orElse(null));
if (alarm.getId() == null) {
// Atomic update and return alarm + assignee.
Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType());
if (existing == null || existing.getStatus().isCleared()) {
if (!alarmCreationEnabled) {
throw new ApiUsageLimitsExceededException("Alarms creation is disabled");
}
return createAlarm(alarm);
} else {
return updateAlarm(existing, alarm);
}
} else {
return updateAlarm(alarm);
}
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmDao.findLatestActiveByOriginatorAndType(tenantId, originator, type);
}
@Override
public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmDao.findLatestByOriginatorAndTypeAsync(tenantId, originator, type);
}
@Override
public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
@ -248,27 +207,6 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
}
}
@Override
@Transactional
public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) {
log.debug("Deleting Alarm Id: {}", alarmId);
Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId());
if (alarm == null) {
return new AlarmOperationResult(alarm, false);
}
AlarmOperationResult result = new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm)));
deleteEntityRelations(tenantId, alarm.getId());
alarmDao.removeById(tenantId, alarm.getUuidId());
return result;
}
private AlarmOperationResult createAlarm(Alarm alarm) throws InterruptedException, ExecutionException {
log.debug("New Alarm : {}", alarm);
Alarm saved = alarmDao.save(alarm.getTenantId(), alarm);
List<EntityId> propagatedEntitiesList = createEntityAlarmRecords(saved);
return new AlarmOperationResult(saved, true, true, propagatedEntitiesList);
}
private List<EntityId> createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException {
Set<EntityId> propagatedEntitiesSet = new LinkedHashSet<>();
propagatedEntitiesSet.add(alarm.getOriginator());
@ -299,61 +237,6 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
return relations.map(EntityRelation::getFrom).collect(Collectors.toCollection(LinkedHashSet::new));
}
private AlarmOperationResult updateAlarm(Alarm update) {
alarmDataValidator.validate(update, Alarm::getTenantId);
return getAndUpdate(update.getTenantId(), update.getId(),
(alarm) -> alarm == null ? null : updateAlarm(alarm, update));
}
private AlarmOperationResult updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
boolean propagationEnabled = !oldAlarm.isPropagate() && newAlarm.isPropagate();
boolean propagationToOwnerEnabled = !oldAlarm.isPropagateToOwner() && newAlarm.isPropagateToOwner();
boolean propagationToTenantEnabled = !oldAlarm.isPropagateToTenant() && newAlarm.isPropagateToTenant();
AlarmSeverity oldAlarmSeverity = oldAlarm.getSeverity();
Alarm result = alarmDao.save(newAlarm.getTenantId(), merge(oldAlarm, newAlarm));
List<EntityId> propagatedEntitiesList;
if (propagationEnabled || propagationToOwnerEnabled || propagationToTenantEnabled) {
try {
propagatedEntitiesList = createEntityAlarmRecords(result);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to update alarm relations [{}]", result, e);
throw new RuntimeException(e);
}
} else {
propagatedEntitiesList = new ArrayList<>(getPropagationEntityIds(result));
}
return new AlarmOperationResult(result, true, false, oldAlarmSeverity, propagatedEntitiesList);
}
@Override
public ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTime) {
Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId());
if (alarm == null || alarm.getStatus().isAck()) {
return Futures.immediateFuture(new AlarmOperationResult(alarm, false));
} else {
alarm.setAcknowledged(true);
alarm.setAckTs(ackTime);
alarm = alarmDao.save(alarm.getTenantId(), alarm);
return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))));
}
}
@Override
public ListenableFuture<AlarmOperationResult> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTime) {
Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId());
if (alarm == null || alarm.getStatus().isCleared()) {
return Futures.immediateFuture(new AlarmOperationResult(alarm, false));
} else {
alarm.setCleared(true);
alarm.setClearTs(clearTime);
if (details != null) {
alarm.setDetails(details);
}
alarm = alarmDao.save(alarm.getTenantId(), alarm);
return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))));
}
}
@Override
public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) {
var result = withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime));
@ -396,23 +279,23 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query) {
return Futures.immediateFuture(alarmDao.findAlarms(tenantId, query));
public PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query) {
return alarmDao.findAlarms(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query));
public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
return alarmDao.findCustomerAlarms(tenantId, customerId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findAlarmsV2(tenantId, query));
public PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmDao.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query));
public PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmDao.findCustomerAlarmsV2(tenantId, customerId, query);
}
@Override
@ -440,9 +323,16 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
@Override
public void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteEntityAlarms [{}]", entityId);
alarmDao.deleteEntityAlarmRecords(tenantId, entityId);
}
@Override
public void deleteEntityAlarmRecordsByTenantId(TenantId tenantId) {
log.trace("Executing deleteEntityAlarmRecordsByTenantId [{}]", tenantId);
alarmDao.deleteEntityAlarmRecordsByTenantId(tenantId);
}
@Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) {
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);

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

@ -26,7 +26,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
@ -201,21 +200,25 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
@Transactional
public void deleteAsset(TenantId tenantId, AssetId assetId) {
log.trace("Executing deleteAsset [{}]", assetId);
validateId(assetId, INCORRECT_ASSET_ID + assetId);
deleteEntityRelations(tenantId, assetId);
Asset asset = assetDao.findById(tenantId, assetId.getId());
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityId(asset.getTenantId(), assetId);
if (entityViews != null && !entityViews.isEmpty()) {
if (entityViewService.existsByTenantIdAndEntityId(tenantId, assetId)) {
throw new DataValidationException("Can't delete asset that has entity views!");
}
Asset asset = assetDao.findById(tenantId, assetId.getId());
alarmService.deleteEntityAlarmRelations(tenantId, assetId);
deleteAsset(tenantId, asset);
}
private void deleteAsset(TenantId tenantId, Asset asset) {
log.trace("Executing deleteAsset [{}]", asset.getId());
relationService.deleteEntityRelations(tenantId, asset.getAssetProfileId());
assetDao.removeById(tenantId, asset.getUuidId());
publishEvictEvent(new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), null));
countService.publishCountEntityEvictEvent(tenantId, EntityType.ASSET);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(assetId).build());
assetDao.removeById(tenantId, assetId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(asset.getId()).build());
}
@Override
@ -434,21 +437,20 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return assetDao.findAssetsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink);
}
private PaginatedRemover<TenantId, Asset> tenantAssetsRemover =
new PaginatedRemover<TenantId, Asset>() {
private final PaginatedRemover<TenantId, Asset> tenantAssetsRemover = new PaginatedRemover<>() {
@Override
protected PageData<Asset> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return assetDao.findAssetsByTenantId(id.getId(), pageLink);
}
@Override
protected PageData<Asset> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return assetDao.findAssetsByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Asset entity) {
deleteAsset(tenantId, new AssetId(entity.getId().getId()));
}
};
@Override
protected void removeEntity(TenantId tenantId, Asset asset) {
deleteAsset(tenantId, asset);
}
};
private PaginatedRemover<CustomerId, Asset> customerAssetsUnasigner = new PaginatedRemover<CustomerId, Asset>() {
private final PaginatedRemover<CustomerId, Asset> customerAssetsUnasigner = new PaginatedRemover<CustomerId, Asset>() {
@Override
protected PageData<Asset> findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) {

17
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.cache.CacheExecutorService;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
@ -61,6 +62,7 @@ public class CachedAttributesService implements AttributesService {
public static final String LOCAL_CACHE_TYPE = "caffeine";
private final AttributesDao attributesDao;
private final JpaExecutorService jpaExecutorService;
private final CacheExecutorService cacheExecutorService;
private final DefaultCounter hitCounter;
private final DefaultCounter missCounter;
@ -73,10 +75,12 @@ public class CachedAttributesService implements AttributesService {
private boolean valueNoXssValidation;
public CachedAttributesService(AttributesDao attributesDao,
JpaExecutorService jpaExecutorService,
StatsFactory statsFactory,
CacheExecutorService cacheExecutorService,
TbTransactionalCache<AttributeCacheKey, AttributeKvEntry> cache) {
this.attributesDao = attributesDao;
this.jpaExecutorService = jpaExecutorService;
this.cacheExecutorService = cacheExecutorService;
this.cache = cache;
@ -134,12 +138,14 @@ public class CachedAttributesService implements AttributesService {
}
@Override
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, Collection<String> attributeKeys) {
public ListenableFuture<List<AttributeKvEntry>> find(TenantId tenantId, EntityId entityId, String scope, final Collection<String> attributeKeysNonUnique) {
validate(entityId, scope);
attributeKeys = new LinkedHashSet<>(attributeKeys); // deduplicate the attributes
final var attributeKeys = new LinkedHashSet<>(attributeKeysNonUnique); // deduplicate the attributes
attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey));
Map<String, TbCacheValueWrapper<AttributeKvEntry>> wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys);
//CacheExecutor for Redis or DirectExecutor for local Caffeine
return Futures.transformAsync(cacheExecutor.submit(() -> findCachedAttributes(entityId, scope, attributeKeys)),
wrappedCachedAttributes -> {
List<AttributeKvEntry> cachedAttributes = wrappedCachedAttributes.values().stream()
.map(TbCacheValueWrapper::get)
@ -155,7 +161,8 @@ public class CachedAttributesService implements AttributesService {
List<AttributeCacheKey> notFoundKeys = notFoundAttributeKeys.stream().map(k -> new AttributeCacheKey(scope, entityId, k)).collect(Collectors.toList());
return cacheExecutor.submit(() -> {
// DB call should run in DB executor, not in cache-related executor
return jpaExecutorService.submit(() -> {
var cacheTransaction = cache.newTransactionForKeys(notFoundKeys);
try {
log.trace("[{}][{}] Lookup attributes from db: {}", entityId, scope, notFoundAttributeKeys);
@ -179,6 +186,8 @@ public class CachedAttributesService implements AttributesService {
throw e;
}
});
}, MoreExecutors.directExecutor()); // cacheExecutor analyse and returns results or submit to DB executor
}
private Map<String, TbCacheValueWrapper<AttributeKvEntry>> findCachedAttributes(EntityId entityId, String scope, Collection<String> attributeKeys) {

4
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java

@ -18,6 +18,8 @@ package org.thingsboard.server.dao.dashboard;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityDao;
@ -40,4 +42,6 @@ public interface DashboardDao extends Dao<Dashboard>, TenantEntityDao, Exportabl
List<Dashboard> findByTenantIdAndTitle(UUID tenantId, String title);
PageData<DashboardId> findIdsByTenantId(TenantId tenantId, PageLink pageLink);
}

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

@ -360,19 +360,18 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return dashboardDao.findByTenantIdAndTitle(tenantId.getId(), title);
}
private PaginatedRemover<TenantId, DashboardInfo> tenantDashboardsRemover =
new PaginatedRemover<TenantId, DashboardInfo>() {
@Override
protected PageData<DashboardInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return dashboardInfoDao.findDashboardsByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, DashboardInfo entity) {
deleteDashboard(tenantId, new DashboardId(entity.getUuidId()));
}
};
private final PaginatedRemover<TenantId, DashboardId> tenantDashboardsRemover = new PaginatedRemover<>() {
@Override
protected PageData<DashboardId> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return dashboardDao.findIdsByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, DashboardId dashboardId) {
deleteDashboard(tenantId, dashboardId);
}
};
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {

3
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.Dao;
@ -53,4 +54,6 @@ public interface DeviceCredentialsDao extends Dao<DeviceCredentials> {
*/
DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId);
DeviceCredentials removeByDeviceId(TenantId tenantId, DeviceId deviceId);
}

9
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -400,4 +400,13 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
publishEvictEvent(new DeviceCredentialsEvictEvent(deviceCredentials.getCredentialsId(), null));
}
@Override
public void deleteDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing deleteDeviceCredentialsByDeviceId [{}]", deviceId);
DeviceCredentials credentials = deviceCredentialsDao.removeByDeviceId(tenantId, deviceId);
if (credentials != null) {
publishEvictEvent(new DeviceCredentialsEvictEvent(credentials.getCredentialsId(), null));
}
}
}

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

@ -321,26 +321,27 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Transactional
@Override
public void deleteDevice(final TenantId tenantId, final DeviceId deviceId) {
log.trace("Executing deleteDevice [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
Device device = deviceDao.findById(tenantId, deviceId.getId());
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), null);
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityId(device.getTenantId(), deviceId);
if (entityViews != null && !entityViews.isEmpty()) {
if (entityViewService.existsByTenantIdAndEntityId(tenantId, deviceId)) {
throw new DataValidationException("Can't delete device that has entity views!");
}
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId);
if (deviceCredentials != null) {
deviceCredentialsService.deleteDeviceCredentials(tenantId, deviceCredentials);
}
deleteEntityRelations(tenantId, deviceId);
deviceDao.removeById(tenantId, deviceId.getId());
Device device = deviceDao.findById(tenantId, deviceId.getId());
alarmService.deleteEntityAlarmRelations(tenantId, deviceId);
deleteDevice(tenantId, device);
}
private void deleteDevice(TenantId tenantId, Device device) {
log.trace("Executing deleteDevice [{}]", device.getId());
deviceCredentialsService.deleteDeviceCredentialsByDeviceId(tenantId, device.getId());
relationService.deleteEntityRelations(tenantId, device.getId());
deviceDao.removeById(tenantId, device.getUuidId());
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), null);
publishEvictEvent(deviceCacheEvictEvent);
countService.publishCountEntityEvictEvent(tenantId, EntityType.DEVICE);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(deviceId).entity(device).build());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(device.getId()).entity(device).build());
}
@Override
@ -658,21 +659,20 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
deleteDevice(tenantId, (DeviceId) id);
}
private PaginatedRemover<TenantId, Device> tenantDevicesRemover =
new PaginatedRemover<>() {
private final PaginatedRemover<TenantId, Device> tenantDevicesRemover = new PaginatedRemover<>() {
@Override
protected PageData<Device> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return deviceDao.findDevicesByTenantId(id.getId(), pageLink);
}
@Override
protected PageData<Device> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return deviceDao.findDevicesByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Device entity) {
deleteDevice(tenantId, new DeviceId(entity.getUuidId()));
}
};
@Override
protected void removeEntity(TenantId tenantId, Device device) {
deleteDevice(tenantId, device);
}
};
private PaginatedRemover<CustomerId, Device> customerDeviceUnasigner = new PaginatedRemover<>() {
private final PaginatedRemover<CustomerId, Device> customerDeviceUnasigner = new PaginatedRemover<>() {
@Override
protected PageData<Device> findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) {

2
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -74,9 +74,7 @@ public abstract class AbstractEntityService {
}
protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
relationService.deleteEntityRelations(tenantId, entityId);
log.trace("Executing deleteEntityAlarms [{}]", entityId);
alarmService.deleteEntityAlarmRelations(tenantId, entityId);
}

2
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java

@ -148,6 +148,8 @@ public interface EntityViewDao extends Dao<EntityView>, ExportableEntityDao<Enti
List<EntityView> findEntityViewsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
/**
* Find tenants entity view types.
*

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

@ -312,6 +312,11 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
EntityViewCacheValue::getEntityViews, v -> new EntityViewCacheValue(null, v), true);
}
@Override
public boolean existsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId) {
return entityViewDao.existsByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
}
@Override
@Transactional
public void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) {

8
dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java

@ -92,12 +92,8 @@ public class BaseEventService implements EventService {
private <T extends Event> void truncateField(T event, Function<T, String> getter, BiConsumer<T, String> setter) {
var str = getter.apply(event);
if (StringUtils.isNotEmpty(str)) {
var length = str.length();
if (length > maxDebugEventSymbols) {
setter.accept(event, str.substring(0, maxDebugEventSymbols) + "...[truncated " + (length - maxDebugEventSymbols) + " symbols]");
}
}
str = StringUtils.truncate(str, maxDebugEventSymbols);
setter.accept(event, str);
}
@Override

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

@ -44,6 +44,7 @@ import java.util.Optional;
public class DefaultNotificationRequestService implements NotificationRequestService, EntityDaoService {
private final NotificationRequestDao notificationRequestDao;
private final NotificationDao notificationDao;
private final ApplicationEventPublisher eventPublisher;
@ -85,10 +86,10 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
return notificationRequestDao.findByRuleIdAndOriginatorEntityId(tenantId, ruleId, originatorEntityId);
}
// ON DELETE CASCADE is used: notifications for request are deleted as well
@Override
public void deleteNotificationRequest(TenantId tenantId, NotificationRequest request) {
notificationRequestDao.removeById(tenantId, request.getUuidId());
notificationDao.deleteByRequestId(tenantId, request.getId());
if (request.isScheduled()) {
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(request.getId()).build());
}
@ -104,6 +105,7 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
notificationRequestDao.updateById(tenantId, requestId, requestStatus, stats);
}
// notifications themselves are left in the database until removed by ttl
@Override
public void deleteNotificationRequestsByTenantId(TenantId tenantId) {
notificationRequestDao.removeByTenantId(tenantId);
@ -119,7 +121,6 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
return EntityType.NOTIFICATION_REQUEST;
}
private static class NotificationRequestValidator extends DataValidator<NotificationRequest> {
}

4
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java

@ -41,4 +41,8 @@ public interface NotificationDao extends Dao<Notification> {
int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status);
void deleteByRequestId(TenantId tenantId, NotificationRequestId requestId);
void deleteByRecipientId(TenantId tenantId, UserId recipientId);
}

3
dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java

@ -15,12 +15,11 @@
*/
package org.thingsboard.server.dao.service;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
public abstract class PaginatedRemover<I, D extends IdBased<?>> {
public abstract class PaginatedRemover<I, D> {
private static final int DEFAULT_LIMIT = 100;

4
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java

@ -34,4 +34,8 @@ public interface EntityAlarmRepository extends JpaRepository<EntityAlarmEntity,
@Modifying
@Query("DELETE FROM EntityAlarmEntity e where e.entityId = :entityId")
void deleteByEntityId(@Param("entityId") UUID entityId);
@Transactional
void deleteByTenantId(UUID tenantId);
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -310,10 +310,14 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override
public void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId) {
log.trace("[{}] Try to delete entity alarm records using [{}]", tenantId, entityId);
entityAlarmRepository.deleteByEntityId(entityId.getId());
}
@Override
public void deleteEntityAlarmRecordsByTenantId(TenantId tenantId) {
entityAlarmRepository.deleteByTenantId(tenantId.getId());
}
@Override
public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) {
AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation());

3
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java

@ -40,4 +40,7 @@ public interface DashboardRepository extends JpaRepository<DashboardEntity, UUID
@Query("SELECT externalId FROM DashboardEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT d.id FROM DashboardEntity d WHERE d.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java

@ -80,6 +80,11 @@ public class JpaDashboardDao extends JpaAbstractDao<DashboardEntity, Dashboard>
return DaoUtil.convertDataList(dashboardRepository.findByTenantIdAndTitle(tenantId, title));
}
@Override
public PageData<DashboardId> findIdsByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(dashboardRepository.findIdsByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)).map(DashboardId::new));
}
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;

8
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java

@ -16,6 +16,9 @@
package org.thingsboard.server.dao.sql.device;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity;
import java.util.UUID;
@ -28,4 +31,9 @@ public interface DeviceCredentialsRepository extends JpaRepository<DeviceCredent
DeviceCredentialsEntity findByDeviceId(UUID deviceId);
DeviceCredentialsEntity findByCredentialsId(String credentialsId);
@Transactional
@Query(value = "DELETE FROM device_credentials WHERE device_id = :deviceId RETURNING *", nativeQuery = true)
DeviceCredentialsEntity deleteByDeviceId(@Param("deviceId") UUID deviceId);
}

7
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java

@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.DaoUtil;
@ -66,4 +67,10 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao<DeviceCredentialsEnt
public DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId) {
return DaoUtil.getData(deviceCredentialsRepository.findByCredentialsId(credentialsId));
}
@Override
public DeviceCredentials removeByDeviceId(TenantId tenantId, DeviceId deviceId) {
return DaoUtil.getData(deviceCredentialsRepository.deleteByDeviceId(deviceId.getId()));
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -118,6 +118,8 @@ public interface EntityViewRepository extends JpaRepository<EntityViewEntity, UU
List<EntityViewEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
@Query("SELECT DISTINCT ev.type FROM EntityViewEntity ev WHERE ev.tenantId = :tenantId")
List<String> findTenantEntityViewTypes(@Param("tenantId") UUID tenantId);

5
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -165,6 +165,11 @@ public class JpaEntityViewDao extends JpaAbstractDao<EntityViewEntity, EntityVie
entityViewRepository.findAllByTenantIdAndEntityId(tenantId, entityId));
}
@Override
public boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId) {
return entityViewRepository.existsByTenantIdAndEntityId(tenantId, entityId);
}
@Override
public ListenableFuture<List<EntitySubtype>> findTenantEntityViewTypesAsync(UUID tenantId) {
return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ENTITY_VIEW, entityViewRepository.findTenantEntityViewTypes(tenantId)));

10
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -104,6 +104,16 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
return notificationRepository.updateStatusByRecipientId(recipientId.getId(), status);
}
@Override
public void deleteByRequestId(TenantId tenantId, NotificationRequestId requestId) {
notificationRepository.deleteByRequestId(requestId.getId());
}
@Override
public void deleteByRecipientId(TenantId tenantId, UserId recipientId) {
notificationRepository.deleteByRecipientId(recipientId.getId());
}
@Override
protected Class<NotificationEntity> getEntityClass() {
return NotificationEntity.class;

6
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java

@ -61,6 +61,12 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Transactional
int deleteByIdAndRecipientId(UUID id, UUID recipientId);
@Transactional
void deleteByRequestId(UUID requestId);
@Transactional
void deleteByRecipientId(UUID recipientId);
@Modifying
@Transactional
@Query("UPDATE NotificationEntity n SET n.status = :status " +

7
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java

@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.UserCredentialsEntity;
@ -62,4 +63,10 @@ public class JpaUserCredentialsDao extends JpaAbstractDao<UserCredentialsEntity,
public UserCredentials findByResetToken(TenantId tenantId, String resetToken) {
return DaoUtil.getData(userCredentialsRepository.findByResetToken(resetToken));
}
@Override
public void removeByUserId(TenantId tenantId, UserId userId) {
userCredentialsRepository.removeByUserId(userId.getId());
}
}

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

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.UserCredentialsEntity;
import java.util.UUID;
@ -30,4 +31,8 @@ public interface UserCredentialsRepository extends JpaRepository<UserCredentials
UserCredentialsEntity findByActivateToken(String activateToken);
UserCredentialsEntity findByResetToken(String resetToken);
@Transactional
void removeByUserId(UUID userId);
}

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/sql/SqlPartitioningRepository.java

@ -140,7 +140,7 @@ public class SqlPartitioningRepository {
try {
partitions.add(Long.parseLong(partitionTsStr));
} catch (NumberFormatException nfe) {
log.warn("Failed to parse table name: {}", partitionTableName);
log.debug("Failed to parse table name: {}", partitionTableName);
}
}
return partitions;

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

@ -248,7 +248,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
publishEvictEvent(new TenantEvictEvent(tenantId, true));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID)
.entity(tenant).entityId(tenantId).build());
deleteEntityRelations(tenantId, tenantId);
relationService.deleteEntityRelations(tenantId, tenantId);
alarmService.deleteEntityAlarmRecordsByTenantId(tenantId);
}
@Override

34
dao/src/main/java/org/thingsboard/server/dao/usagerecord/DefaultApiLimitService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.usagerecord;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -27,6 +28,8 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
public class DefaultApiLimitService implements ApiLimitService {
@ -36,16 +39,31 @@ public class DefaultApiLimitService implements ApiLimitService {
@Override
public boolean checkEntitiesLimit(TenantId tenantId, EntityType entityType) {
DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
long limit = profileConfiguration.getEntitiesLimit(entityType);
if (limit > 0) {
EntityTypeFilter filter = new EntityTypeFilter();
filter.setEntityType(entityType);
long currentCount = entityService.countEntitiesByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), new EntityCountQuery(filter));
return currentCount < limit;
} else {
long limit = getLimit(tenantId, profileConfiguration -> profileConfiguration.getEntitiesLimit(entityType));
if (limit <= 0) {
return true;
}
EntityTypeFilter filter = new EntityTypeFilter();
filter.setEntityType(entityType);
long currentCount = entityService.countEntitiesByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), new EntityCountQuery(filter));
return currentCount < limit;
}
@Override
public long getLimit(TenantId tenantId, Function<DefaultTenantProfileConfiguration, Number> extractor) {
if (tenantId == null || tenantId.isSysTenantId()) {
return 0L;
}
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
if (tenantProfile == null) {
throw new IllegalArgumentException("Tenant profile not found for tenant " + tenantId);
}
Number value = extractor.apply(tenantProfile.getDefaultProfileConfiguration());
if (value == null) {
return 0L;
}
return Math.max(0, value.longValue());
}
}

3
dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.user;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.Dao;
@ -58,4 +59,6 @@ public interface UserCredentialsDao extends Dao<UserCredentials> {
*/
UserCredentials findByResetToken(TenantId tenantId, String resetToken);
void removeByUserId(TenantId tenantId, UserId userId);
}

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

@ -252,10 +252,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
UserId userId = user.getId();
log.trace("[{}] Executing deleteUser [{}]", tenantId, userId);
validateId(userId, INCORRECT_USER_ID + userId);
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId());
userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
userCredentialsDao.removeByUserId(tenantId, userId);
userAuthSettingsDao.removeByUserId(userId);
deleteEntityRelations(tenantId, userId);
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.USER);
@ -446,8 +446,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
}
@Override
protected void removeEntity(TenantId tenantId, User entity) {
deleteUser(tenantId, entity);
protected void removeEntity(TenantId tenantId, User user) {
deleteUser(tenantId, user);
}
};

4
dao/src/main/resources/sql/schema-entities-idx.sql

@ -104,6 +104,8 @@ CREATE INDEX IF NOT EXISTS idx_notification_rule_tenant_id_trigger_type_created_
CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_user_created_time ON notification_request(tenant_id, created_time DESC)
WHERE originator_entity_type = 'USER';
CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id ON notification_request(tenant_id);
CREATE INDEX IF NOT EXISTS idx_notification_request_rule_id_originator_entity_id ON notification_request(rule_id, originator_entity_id)
WHERE originator_entity_type = 'ALARM';
@ -112,6 +114,8 @@ CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_reque
CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notification(request_id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';

4
dao/src/main/resources/sql/schema-entities.sql

@ -853,8 +853,8 @@ CREATE TABLE IF NOT EXISTS notification_request (
CREATE TABLE IF NOT EXISTS notification (
id UUID NOT NULL,
created_time BIGINT NOT NULL,
request_id UUID NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE,
request_id UUID,
recipient_id UUID NOT NULL,
type VARCHAR(50) NOT NULL,
subject VARCHAR(255),
body VARCHAR(1000) NOT NULL,

11
dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.TenantId;
@ -60,11 +61,12 @@ public class AlarmCommentServiceTest extends AbstractServiceTest {
@Before
public void before() {
alarm = Alarm.builder().tenantId(tenantId).originator(new AssetId(Uuids.timeBased()))
alarm = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
.originator(new AssetId(Uuids.timeBased()))
.type(TEST_ALARM)
.severity(AlarmSeverity.CRITICAL)
.startTs(System.currentTimeMillis()).build();
alarm = alarmService.createOrUpdateAlarm(alarm).getAlarm();
.startTs(System.currentTimeMillis()).build()).getAlarm();
user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
@ -77,10 +79,9 @@ public class AlarmCommentServiceTest extends AbstractServiceTest {
@After
public void after() {
alarmService.deleteAlarm(tenantId, alarm.getId());
alarmService.delAlarm(tenantId, alarm.getId());
}
@Test
public void testCreateAndFetchAlarmComment() throws ExecutionException, InterruptedException {
AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId())

52
dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java

@ -146,7 +146,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -157,7 +157,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -171,7 +171,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -182,7 +182,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -195,7 +195,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_ACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -206,7 +206,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -218,7 +218,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.CLEARED_ACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -245,11 +245,11 @@ public class AlarmServiceTest extends AbstractServiceTest {
// Check child relation
PageData<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -261,7 +261,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -276,7 +276,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -288,7 +288,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -302,7 +302,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.ACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -314,7 +314,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -327,7 +327,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -371,7 +371,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.fetchOriginator(true)
.pageLink(new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, alarms.getData().get(0));
@ -466,7 +466,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(tenantAlarm, new AlarmInfo(alarms.getData().get(0)));
@ -752,7 +752,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarmsInfoData.getData());
Assert.assertEquals(1, alarmsInfoData.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0)));
@ -763,7 +763,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarmsInfoData.getData());
Assert.assertEquals(1, alarmsInfoData.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0)));
@ -774,7 +774,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarmsInfoData.getData());
Assert.assertEquals(1, alarmsInfoData.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0)));
@ -899,7 +899,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -910,12 +910,12 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()).isSuccessful());
Assert.assertTrue("Alarm was not deleted when expected", alarmService.delAlarm(tenantId, created.getId()).isSuccessful());
Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get();
@ -926,7 +926,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -936,7 +936,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());

50
dao/src/test/java/org/thingsboard/server/dao/service/TbCacheSerializationTest.java

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2023 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.server.dao.service;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@DaoSqlTest
public class TbCacheSerializationTest extends AbstractServiceTest {
@Autowired
TbTransactionalCache<TenantId, PageData<EntitySubtype>> alarmTypesCache;
@Test
public void AlarmTypesSerializationTest() {
var typesCount = 13;
TenantId tenantId = new TenantId(UUID.randomUUID());
List<EntitySubtype> types = new ArrayList<>(typesCount);
for (int i = 0; i < typesCount; i++) {
types.add(new EntitySubtype(tenantId, EntityType.ALARM, "alarm_type_" + i));
}
PageData<EntitySubtype> alarmTypesPage = new PageData<>(types, 1, typesCount, false);
alarmTypesCache.put(tenantId, alarmTypesPage);
PageData<EntitySubtype> foundAlarmTypes = alarmTypesCache.get(tenantId).get();
Assert.assertEquals(alarmTypesPage, foundAlarmTypes);
}
}

1
pull_request_template.md

@ -26,6 +26,7 @@ Put your PR description here instead of this sentence.
- [ ] If new dependency was added: the dependency tree is checked for conflicts.
- [ ] If new service was added: the service is marked with corresponding @TbCoreComponent, @TbRuleEngineComponent, @TbTransportComponent, etc.
- [ ] If new REST API was added: the RestClient.java was updated, issue for [Python REST client](https://github.com/thingsboard/thingsboard-python-rest-client) is created.
- [ ] If new yml property was added: make sure a description is added (above or near the property).

26
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java

@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import java.util.Collection;
@ -70,21 +69,6 @@ public interface RuleEngineAlarmService {
AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs);
/*
* Legacy API, before 3.5.
*/
@Deprecated(since = "3.5", forRemoval = true)
Alarm createOrUpdateAlarm(Alarm alarm);
@Deprecated(since = "3.5", forRemoval = true)
ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
@Deprecated(since = "3.5", forRemoval = true)
ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
@Deprecated(since = "3.5", forRemoval = true)
ListenableFuture<AlarmOperationResult> clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
// Other API
Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId);
@ -94,7 +78,7 @@ public interface RuleEngineAlarmService {
Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId);
@ -102,13 +86,13 @@ public interface RuleEngineAlarmService {
return Futures.immediateFuture(findAlarmInfoById(tenantId, alarmId));
}
ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query);
PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId);

122
tools/src/main/python/check_yml_file.py

@ -0,0 +1,122 @@
#
# Copyright © 2016-2023 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 sys
import re
def extract_properties_with_comments(yaml_file_path):
properties = {}
with open(yaml_file_path, 'r') as file:
lines = file.readlines()
index = 0
key_level_map = {0 : ''}
parse_line('', '', key_level_map, 0, index, lines, properties)
return properties
def parse_line(table_name, comment, key_level_map, parent_line_level, index, lines, properties):
if index >= len(lines):
return
line = lines[index]
line_level = (len(line) - len(line.lstrip())) if line.strip() else 0
line = line.strip()
# if line is empty - parse next line
if not line:
index = index + 1
parse_line(table_name, comment, key_level_map, line_level, index, lines, properties)
# if line is a comment - save comment and parse next line
else:
if line_level == 0:
key_level_map = {0 : ''}
if line.startswith('#'):
if line_level == 0:
table_name = line.lstrip('#')
elif line_level == parent_line_level:
comment = comment + '\n' + line.lstrip('#')
else:
comment = line.lstrip('#')
index = index + 1
parse_line(table_name, comment, key_level_map, line_level, index, lines, properties)
else:
# Check if it's a property line
if ':' in line:
# clean comment if level was changed
if line_level != parent_line_level:
comment = ''
key, value = line.split(':', 1)
if key.startswith('- '):
key = key.lstrip('- ')
key_level_map[line_level] = key
value = value.strip()
if value.split('#')[0]:
current_key = ''
for k in key_level_map.keys():
if k <= line_level:
current_key = ((current_key + '.') if current_key else '') + key_level_map[k]
properties[current_key] = (value, comment, table_name)
comment = ''
index = index + 1
parse_line(table_name, comment, key_level_map, line_level, index, lines, properties)
def extract_property_info(properties):
rows = []
for property_name, value in properties.items():
if '#' in value[0]:
value_parts = value[0].split('#')
comment = value_parts[1]
else:
comment = value[1]
pattern = r'\"\$\{(.*?)\:(.*?)\}\"'
match = re.match(pattern, value[0])
if match is not None:
rows.append((property_name, match.group(1), match.group(2), comment, value[2]))
else:
rows.append((property_name, "", value[0].split('#')[0], comment, value[2]))
return rows
def check_descriptions(properties):
variables_without_description = []
for row in properties:
# Extract information from the tuple
property_name, env_variable, default_value, comment, table_name = row
if comment == '' or len(comment) < 5 :
variables_without_description.append(property_name)
return variables_without_description
if __name__ == '__main__':
sys. setrecursionlimit(10000)
# path to the YAML file
input_yaml_file = "application/src/main/resources/thingsboard.yml"
# Parse yml file to map where key is property key path with '.' separator
# and value is an object (env_name_with_default_value, comment, table_name)
properties = extract_properties_with_comments(input_yaml_file)
# Extract property information (extract env name, default value and comment nearby property)
property_info = extract_property_info(properties)
# Check all properies have descriptions
variables_without_desc = check_descriptions(property_info)
if len(variables_without_desc) > 0:
print(f"There are some yml properties without valid description: (total {len(variables_without_desc)}) {variables_without_desc}.")
exit(1)
else:
print("All yml properties have valid description.")

1
ui-ngx/package.json

@ -44,6 +44,7 @@
"@ngrx/store-devtools": "^15.4.0",
"@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@svgdotjs/svg.js": "^3.2.0",
"@tinymce/tinymce-angular": "^7.0.0",
"ace-builds": "1.4.13",
"ace-diff": "^3.0.3",

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

@ -768,7 +768,7 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
if (datasource) {
if (datasource.dataKeys) {
datasource.dataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
if (dataKey?.label === label) {
i++;
label = name + ' ' + i;
matches = true;
@ -777,7 +777,7 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
}
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
if (dataKey?.label === label) {
i++;
label = name + ' ' + i;
matches = true;

28
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -262,6 +262,34 @@
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.queue-stats-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="queueStatsTtlDays"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('queueStatsTtlDays').hasError('required')">
{{ 'tenant-profile.queue-stats-ttl-days-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('queueStatsTtlDays').hasError('min')">
{{ 'tenant-profile.queue-stats-ttl-days-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill" subscriptSizing="dynamic">
<mat-label translate>tenant-profile.rule-engine-exceptions-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="ruleEngineExceptionsTtlDays"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('ruleEngineExceptionsTtlDays').hasError('required')">
{{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('ruleEngineExceptionsTtlDays').hasError('min')">
{{ 'tenant-profile.rule-engine-exceptions-ttl-days-days-range' | translate}}
</mat-error>
<mat-hint></mat-hint>
</mat-form-field>
</div>
</fieldset>
<fieldset class="fields-group">

2
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -90,6 +90,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]],
alarmsTtlDays: [null, [Validators.required, Validators.min(0)]],
rpcTtlDays: [null, [Validators.required, Validators.min(0)]],
queueStatsTtlDays: [null, [Validators.required, Validators.min(0)]],
ruleEngineExceptionsTtlDays: [null, [Validators.required, Validators.min(0)]],
tenantServerRestLimitsConfiguration: [null, []],
customerServerRestLimitsConfiguration: [null, []],
maxWsSessionsPerTenant: [null, [Validators.min(0)]],

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -58,6 +58,9 @@ import {
import {
BatteryLevelBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/battery-level-basic-config.component';
import {
WindSpeedDirectionBasicConfigComponent
} from '@home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component';
@NgModule({
declarations: [
@ -75,7 +78,8 @@ import {
DataKeysPanelComponent,
AlarmCountBasicConfigComponent,
EntityCountBasicConfigComponent,
BatteryLevelBasicConfigComponent
BatteryLevelBasicConfigComponent,
WindSpeedDirectionBasicConfigComponent
],
imports: [
CommonModule,
@ -97,7 +101,8 @@ import {
DataKeysPanelComponent,
AlarmCountBasicConfigComponent,
EntityCountBasicConfigComponent,
BatteryLevelBasicConfigComponent
BatteryLevelBasicConfigComponent,
WindSpeedDirectionBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -113,5 +118,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-aggregated-value-card-basic-config': AggregatedValueCardBasicConfigComponent,
'tb-alarm-count-basic-config': AlarmCountBasicConfigComponent,
'tb-entity-count-basic-config': EntityCountBasicConfigComponent,
'tb-battery-level-basic-config': BatteryLevelBasicConfigComponent
'tb-battery-level-basic-config': BatteryLevelBasicConfigComponent,
'tb-wind-speed-direction-basic-config': WindSpeedDirectionBasicConfigComponent
};

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html

@ -61,7 +61,7 @@
</tb-font-settings>
</div>
<div class="tb-color-field">
<tb-color-settings formControlName="color">
<tb-color-settings formControlName="color" settingsKey="{{ (index+1) }}. {{ aggregatedValueCardKeyPositionTranslationMap.get(keyRowFormGroup.get('position').value) | translate }}">
</tb-color-settings>
</div>
<div class="tb-arrow-field">

4
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

@ -87,6 +87,9 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
@Input()
keyName: string;
@Input()
index: number;
@Output()
keyRemoved = new EventEmitter();
@ -225,6 +228,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
this.updateModel();
this.cd.markForCheck();
}
});
}

1
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html

@ -32,6 +32,7 @@
<div *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-aggregated-data-key-row fxFlex
[formControl]="keyControl"
[index]="$index"
[datasourceType]="datasourceType"
[keyName]="keyName"
(keyRemoved)="removeKey($index)">

5
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html

@ -29,6 +29,11 @@
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.aggregated-value-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts

@ -119,6 +119,7 @@ export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigCo
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
autoScale: [settings.autoScale, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
@ -172,6 +173,7 @@ export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigCo
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.showSubtitle = config.showSubtitle;
this.widgetConfig.config.settings.subtitle = config.subtitle;
this.widgetConfig.config.settings.subtitleFont = config.subtitleFont;

13
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -40,6 +40,11 @@
{{ valueCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.value-card.label' | translate }}
@ -51,7 +56,7 @@
<tb-font-settings formControlName="labelFont"
[previewText]="valueCardWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
<tb-color-settings formControlName="labelColor" settingsKey="{{'widgets.value-card.label' | translate }}">
</tb-color-settings>
</div>
</div>
@ -69,7 +74,7 @@
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
<tb-color-settings formControlName="iconColor" settingsKey="{{'widgets.value-card.icon' | translate }}">
</tb-color-settings>
</div>
</div>
@ -84,7 +89,7 @@
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.value-card.value' | translate }}">
</tb-color-settings>
</div>
</div>
@ -97,7 +102,7 @@
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
<tb-color-settings formControlName="dateColor" settingsKey="{{'widgets.value-card.date' | translate }}">
</tb-color-settings>
</div>
</div>

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts

@ -116,6 +116,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
showLabel: [settings.showLabel, []],
label: [getLabel(configData.config.datasources), []],
@ -154,6 +155,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.showLabel = config.showLabel;
setLabel(config.label, this.widgetConfig.config.datasources);

37
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row">
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row" [class]="{'tb-single-row': singleRow}">
<mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>
@ -23,8 +23,8 @@
</mat-select>
</mat-form-field>
<mat-form-field class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
<mat-chip-grid #chipList [formControl]="keysFormControl">
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type"
(removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels">
@ -67,8 +67,8 @@
#keyInput
[formControl]="keyFormControl"
matAutocompleteOrigin
[fxHide]="!!modelValue.type"
[readonly]="!!modelValue.type"
[fxHide]="!!modelValue?.type"
[readonly]="!!modelValue?.type"
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
(focusin)="onKeyInputFocus()"
@ -139,7 +139,7 @@
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<mat-form-field *ngIf="!hideDataKeyLabel" class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div *ngIf="!hideDataKeyColor" class="tb-color-field">
@ -147,25 +147,28 @@
formControlName="color">
</tb-color-input>
</div>
<div *ngIf="!hideUnits" class="tb-units-field">
<div *ngIf="!hideUnits && !hideDataKeyUnits && (!singleRow || displayUnitsOrDigits)" class="tb-units-field">
<tb-unit-input *ngIf="displayUnitsOrDigits"
formControlName="units">
</tb-unit-input>
</div>
<div *ngIf="!hideDecimals" class="tb-decimals-field">
<div *ngIf="!hideDecimals && !hideDataKeyDecimals && (!singleRow || displayUnitsOrDigits)" class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div *ngIf="singleRow" matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell-buttons">
<button fxHide.lt-lg
type="button"
mat-icon-button
(click)="editKey(true)"
[matTooltip]="keySettingsTitle"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<div *ngIf="!singleRow" class="tb-form-table-row-cell-buttons">
<div fxHide.lt-lg class="tb-settings-button">
<button *ngIf="modelValue"
type="button"
mat-icon-button
(click)="editKey(true)"
[matTooltip]="keySettingsTitle"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
</div>
<button type="button"
mat-icon-button
(click)="keyRemoved.emit()"

25
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -88,4 +88,29 @@
display: block;
}
}
.tb-settings-button {
width: 40px;
min-width: 40px;
}
&.tb-single-row {
padding: 0;
.tb-units-field, .tb-decimals-field {
min-width: 0;
width: auto;
}
.tb-units-field {
tb-unit-input {
width: auto;
max-width: 140px;
}
}
.tb-decimals-field {
.mat-mdc-form-field {
width: auto;
max-width: 140px;
}
}
}
}

138
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -35,7 +35,8 @@ import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors
ValidationErrors,
Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
@ -47,7 +48,6 @@ import {
Widget,
widgetType
} from '@shared/models/widget.models';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
@ -65,6 +65,9 @@ import {
import { deepClone } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => {
const dataKey: DataKey = control.value;
@ -104,6 +107,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
required = false;
@Input()
datasourceType: DatasourceType;
@ -113,9 +120,52 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Input()
deviceId: string;
@Input()
@coerceBoolean()
hasAdditionalLatestDataKeys = false;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@Input()
@coerceBoolean()
hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideUnits = false;
@Input()
@coerceBoolean()
hideDecimals = false;
@Input()
@coerceBoolean()
singleRow = true;
@Input()
dataKeyType: DataKeyType;
@Input()
keySettingsTitle: string;
@Input()
removeKeyTitle: string;
@Output()
keyRemoved = new EventEmitter();
keysFormControl: UntypedFormControl;
keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup;
@ -126,33 +176,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
keySearchText = '';
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
private latestKeySearchTextResult: Array<DataKey> = null;
private keyFetchObservable$: Observable<Array<DataKey>> = null;
get dataKeyType(): DataKeyType {
return this.dataKeysPanelComponent.dataKeyType;
}
get alarmKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.alarmKeys;
}
get functionTypeKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.functionTypeKeys;
}
get hideDataKeyColor(): boolean {
return this.dataKeysPanelComponent.hideDataKeyColor;
}
get hideUnits(): boolean {
return this.dataKeysPanelComponent.hideUnits;
}
get hideDecimals(): boolean {
return this.dataKeysPanelComponent.hideDecimals;
}
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
@ -161,10 +190,6 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get hasAdditionalLatestDataKeys(): boolean {
return this.dataKeysPanelComponent.hasAdditionalLatestDataKeys;
}
get widget(): Widget {
return this.widgetConfigComponent.widget;
}
@ -198,19 +223,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
get displayUnitsOrDigits() {
return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type);
}
get keySettingsTitle(): string {
return this.dataKeysPanelComponent.keySettingsTitle;
}
get removeKeyTitle(): string {
return this.dataKeysPanelComponent.removeKeyTitle;
}
get dragEnabled(): boolean {
return this.dataKeysPanelComponent.dragEnabled;
return this.modelValue?.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue?.type);
}
get isLatestDataKeys(): boolean {
@ -224,12 +237,27 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
private cd: ChangeDetectorRef,
public translate: TranslateService,
public truncate: TruncatePipe,
private dataKeysPanelComponent: DataKeysPanelComponent,
private utils: UtilsService,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({
name,
type: DataKeyType.alarm
});
}
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
this.keyFormControl = this.fb.control('');
this.keysFormControl = this.fb.control([], this.required ? [Validators.required] : []);
this.keyRowFormGroup = this.fb.group({
label: [null, []],
color: [null, []],
@ -300,14 +328,16 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keysFormControl.disable({emitEvent: false});
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keysFormControl.enable({emitEvent: false});
this.keyRowFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey): void {
this.modelValue = value || {} as DataKey;
this.modelValue = (value?.name && value?.type) ? value : null;
this.keyRowFormGroup.patchValue(
{
label: value?.label,
@ -321,6 +351,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
latest: (value as any)?.latest
}, {emitEvent: false});
}
this.keysFormControl.patchValue(this.modelValue ? [this.modelValue] : [], {emitEvent: false});
this.cd.markForCheck();
}
@ -368,10 +399,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks,
hideDataKeyLabel: false,
hideDataKeyLabel: this.hideDataKeyLabel,
hideDataKeyColor: this.hideDataKeyColor,
hideDataKeyUnits: !this.displayUnitsOrDigits,
hideDataKeyDecimals: !this.displayUnitsOrDigits
hideDataKeyUnits: this.hideDataKeyUnits || !this.displayUnitsOrDigits,
hideDataKeyDecimals: this.hideDataKeyDecimals || !this.displayUnitsOrDigits
}
}).afterClosed().subscribe((updatedDataKey) => {
if (updatedDataKey) {
@ -387,7 +418,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
removeKey() {
this.modelValue = {} as DataKey;
this.modelValue = null;
this.updateModel();
this.clearKeyChip();
}
@ -409,7 +440,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
onKeyInputFocus() {
if (!this.modelValue.type) {
if (!this.modelValue?.type) {
this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
}
}
@ -479,8 +510,11 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
private updateModel() {
const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.keysFormControl.patchValue(this.modelValue ? [this.modelValue] : [], {emitEvent: false});
if (this.modelValue !== null) {
const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
}
this.propagateChange(this.modelValue);
}

9
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -36,9 +36,18 @@
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"
required
[datasourceType]="datasourceType"
[deviceId]="deviceId"
[entityAliasId]="entityAliasId"
[hasAdditionalLatestDataKeys]="hasAdditionalLatestDataKeys"
[hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals"
[hideUnits]="hideUnits"
[dataKeyType]="dataKeyType"
[singleRow]="false"
[keySettingsTitle]="keySettingsTitle"
[removeKeyTitle]="removeKeyTitle"
(keyRemoved)="removeKey($index)">
</tb-data-key-row>
<div class="tb-form-table-row-cell-buttons">

17
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -41,7 +41,6 @@ import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared
import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@ -113,8 +112,6 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
hideSourceSelection = false;
dataKeyType: DataKeyType;
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
keysListFormGroup: UntypedFormGroup;
@ -165,20 +162,6 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
this.keysListFormGroup.valueChanges.subscribe(
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value)
);
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({
name,
type: DataKeyType.alarm
});
}
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
this.updateParams();
}

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

Loading…
Cancel
Save