Browse Source

Merge remote-tracking branch 'upstream/develop/3.5.2' into feature/microsoft-teams-notifications

pull/8843/head
Vladyslav_Prykhodko 3 years ago
parent
commit
d54320bc79
  1. 8
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  2. 46
      application/src/main/data/json/system/widget_bundles/cards.json
  3. 12
      application/src/main/data/json/system/widget_bundles/charts.json
  4. 2
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  5. 63
      application/src/main/data/upgrade/3.5.1/schema_update.sql
  6. 12
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  7. 30
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  8. 253
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  9. 59
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  10. 2
      application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
  11. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  12. 237
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  13. 18
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java
  14. 35
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
  15. 38
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  16. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  17. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  18. 19
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  19. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  20. 16
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  21. 3
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  22. 8
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  23. 3
      application/src/main/resources/thingsboard.yml
  24. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  25. 47
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  26. 32
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  27. 9
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  28. 4
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  29. 1
      application/src/test/resources/application-test.properties
  30. 40
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java
  31. 34
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java
  32. 26
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java
  33. 35
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java
  34. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java
  35. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  36. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  37. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  38. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  39. 4
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  40. 4
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  41. 4
      common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java
  42. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  43. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  44. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  45. 6
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  46. 1
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java
  47. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
  48. 15
      common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
  49. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  50. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java
  51. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java
  52. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  53. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java
  54. 30
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  55. 4
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  56. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  57. 21
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java
  58. 43
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  59. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  60. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  61. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  62. 8
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  63. 10
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  64. 2
      dao/src/main/resources/sql/schema-entities.sql
  65. 26
      dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java
  66. 40
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  67. 3
      dao/src/test/resources/application-test.properties
  68. 5
      dao/src/test/resources/sql/system-test-psql.sql
  69. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  70. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
  71. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java
  72. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java
  73. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
  74. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  75. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  76. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java
  77. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java
  78. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  79. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  80. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  81. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java
  82. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  83. 1
      ui-ngx/angular.json
  84. 47
      ui-ngx/src/app/app.component.ts
  85. 7
      ui-ngx/src/app/core/http/device.service.ts
  86. 2
      ui-ngx/src/app/core/public-api.ts
  87. 6
      ui-ngx/src/app/core/services/dialog.service.ts
  88. 2142
      ui-ngx/src/app/core/services/material-icons-codepoints.raw
  89. 2
      ui-ngx/src/app/core/services/menu.models.ts
  90. 52
      ui-ngx/src/app/core/services/menu.service.ts
  91. 25
      ui-ngx/src/app/core/services/resources.service.ts
  92. 50
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  93. 75
      ui-ngx/src/app/core/services/utils.service.ts
  94. 37
      ui-ngx/src/app/core/utils.ts
  95. 19
      ui-ngx/src/app/core/ws/public-api.ts
  96. 8
      ui-ngx/src/app/modules/common/modules-map.ts
  97. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  98. 19
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  99. 23
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts
  100. 12
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html

8
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

@ -3,7 +3,9 @@
"alias": "alarm_widgets", "alias": "alarm_widgets",
"title": "Alarm widgets", "title": "Alarm widgets",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAOPElEQVR42u2deVsTVx+G/Xb9ALVeffuHtmql2kVrVWytWltr64KoqIgbolRFRMQNtCwioiLIqii7grIoShARwnbee3Js3pgibzYQzPNcXLmGyUwmc+ae35kE7jOzjDHDw8MvFCVCGRoaAqpZlqqxsTGjKGEHkMAJqGaJKmUy2HLAUlsokY0DVk9PjxpCiWyASmApAksRWIrAEliKwFI+DLD4pst3gbKyss7OTjWcEi5Yubm5X3zxxejoqP01ISGhtLRUDaeEC9ZPP/30xx9/UKj8wBoYGLh9+3Z5ebllrrW1taOj49atW263u6mpqaWl5ebNm/39/c+ePbt+/bq3yD1//vzatWsNDQ1q+qgG6/Hjx+vWrauvr9+8ebMvWPyVccWKFZmZmXv37k1MTGT+rl27li1bduLECfD67LPPjh07lpyc/N133+3YsSMrK2vhwoUvX77s7u5mzvnz59evX3/lyhW1fvSCBR95eXlMAERvb68XLMoSzLW3t9+9e3fx4sUWrJKSEiYAKzY21q4+b948ChsT27Ztq6mpoZKtXLmSFYHs0aNHav0oBYs+jquruLi4PXv2fPPNN1QaL1ivX7/++eefjx49mpaWFhMTY8G6c+eOBWv16tX2FebPn287SupWZWUlE3SdW7duXbVqFVVQrR+lYAEQPWCnJw8ePKCn84IFJfBh+8pFixYFCFZtbS0XXvxaUVFBDVPrRylYf/75p/ea3V7FNzY2WrC4Kl++fDld3u+///7pp59CTyBg0ZlydbVp0yYY9X1lJRo/FU4Qe/0UbAYHB71fXigCS1EEliKwFIGlKAJLEViKwFIUgaUILEVgKYrAUgSWIrAURWApAksRWIoisBSBpXz4YO3evXvleEHameJ3WVBQoEP14YAFQ0HNn7xM/RZHRkaCWp4RLt7j1qMOLLyd77///qOPPlq6dOmpU6dmBFjV1dWYkrzt1NTUwNc6c+YMCi7jXIQv4m7YsIH9xRB++vQptrDfC+JjFhUVqWI5WbBgAboYLYLyigN98ODBnJwchDCU1ydPnvT19WEvopG5XC6/FfGt/bZoBxCf1Ozfv/+vv/7CI2JkAEb/PXLkCO8TrRIpnIYCHexIhp9g5qVLl3DdEHoZZODcuXNYcXPnzv31119Pnz798OHDGzduWDU8qKAwffzxx69evUIc5xVQyeGMRkPDZAKLk+3SnnV1dbxJe7lCecO941lWiUawkKcZwYEDk5KSQnNw2VRYWMhR2bdvH3MOHTrEkfNdC4eR1gQ77xZpbpZn/qS2CEeRQVC+/vpryi2jnvz444/FxcWIkOCSnZ3NBOOaoFJyFBmi4t69exRj9o63SqnjbGEt9vHw4cPIlZw2IbyBq1evfvXVV7wUkO3cuZMXbGtr4zWTkpIOHDgA0zxCGJqn8QxiANw4w7wf63VGHVhM/Pbbb19++SWNAliczRwJ5lCrOOE44/99wgEfvjWaK1u0VE3BUCKXL19mKGnomTNnDiQx9gnvLT8/n5n0j2vXrmX8nNmzZzOTksZ7pkQZz+AUXrA4hehM6ctC2DrbZR+plCDFJixYbJ2CRO2kNFqwQJZhCsw/o2Pgl9OMdAVRChb7D1i2gHOQOC/Bi2PDYaDCc2D+vS6tDE9scWqoMp6BBRiogi1u374dpildHFR7gYj8zTFmgpJGwaAHZzwmX7BYzDYOJ0xo15T0a7/88gutQXVvbm5OT0/nBRkcyl72bdmyhSsKOlwuvBhWg01/8sknjOFD1WQV2zlG3adCPjfR2XGRa8ECKe8nKS5oJvgcRN1ii1M87JHvxdy4n/je9TGQFbno5g1T2CKydTvtuzk7x6/RpuDqM2Jg8X3VuN9jMSZWCFuiaFHbbVtQpWj9wNdl/KMZ9EmboZ0Y9UTfY+mbd0VgKQJLEVgCSxFYisBSBJaiCCxlRoDFXzF143UlsgEqVSxFXaEisBSBJbAUgaUILEVgKYrAUgSWIrD8M30Ue+WDAmv6KPbTKSNmbNLviee1JDAD8N6CXZ07vX/gYCFB4KOiK/GI/BmpN42CPIlN0pRsbi14M125xtRufuvZzlzTM7nCMboi5hl3IcXIOHv2bAh+M45hVFQsa95ZICAM/47GQn1GRqXhsFXxQnkP9LDcvhUP2C6JnFnjCQtwO9aMjAy6Zqxz5EScO3xo5hiPeIi7zKuxOiIQE2zCaoyhg1U027xsMO4eU/wfB6zhl6Z2m6neYPrbTFehcVWb/sfm3iZzP84M95vWDHNvc6RoQ1XFSbRF6+LFixYs7EUkVSsqImTTSjQCFj86K24c06iqqNKsgi3NNI5hFIGFvLtx40YGQUD1ZKwLWoQ/gNtGRNFk2lpl69atwyjn9r4suWbNmlxPcOiQNquqqhDevWekfQQjjE2W5JDQprwswOGZhQVWfaKpTzCt6c4EYIERhaphv2lIMi1HTcffpiLWmfnkvGlJNTUbTddVp4uM0PFAp/b+asFir2kNWg+J3LYDbcgjd3oHph9++IGhHDC2UTVh0SqvUQQWDUR5t2cYYFF7qDSccxYRwKLYGI9DTJMhGbMkjQhVLE8FYj53leaW0l6kLJQWLDuHtRhuhBU5m8MC60mOKVvm/DwvdcDqLDA1Gxyk6ve+Aaskxgy5nPnQBli9dRG8ukK89qtYGNgFnrS3t9t2AB1aiUFWaBkMafssbcUwE1HXFdJSmOmMlnHhwoVxwUJsT/IE69cuSctasBgNgTOSzpFlWJ5SzzlKj0CRYzwML1gM0QG7LBbWWEKA1Z5jWk+ZugTzvNwBq7vEIenOcqfLs2B15pmKFQ55/a2RBctWKXYNm55RGyxY6OPsFDuL8m/b4dtvv7VgsTy/UqqxzBlAhaEl6DSp31H3qfBdZrqtWL7P+i3pa5SPeGLG88ontvXDuPbxvJnhAefSqqvozcfDSQv77nc/9omHI/A+y/XANBm0beoU+4lDSecyYrp/1eB+YVrPvIFMCRksRRFYisBSBJaiCCxFYCkCS1EEljKdwJJir0ixV9QVKgJLDaEILEVgKQJLUQSWIrAUgeUXmdDKpIAV1Sb02/9y7vzq+7/kE9zDDQs5HKvRE5nQ/ycz0oTu7jbcHHX1ahTH/8HETTrz8kxdncGvev3aJCW9c3V8obqwpB2Z0IFmhpnQKSkmJ8eZ4IalbW0mPt6sXeuAVVho0GUXLjS4ZYcPO8whleAc44Bw7+dNm5wf7mAdHlgyoYMGa8aY0Bs3Gt8iMWcOHZLJzXWAKyhwkHrxwixdyj3BTUKC89TZs6a21sGO3fz77zDBkgkdNFgzxoQGHTAimZlOKZo715n+N1gXL5rUVKduUdWYmZhotmyh0oYJlkzooMGaMSY0RWjJEqeP4wdufMGi15s3z+EGsFwu51KMprhwgf7JmV60yNBbhX2NJRM6lMwYE3pwcPz5brff/vhPRCIyoSOWmWFCKxEBS1EEliKwFIGlKAJLEViKwFIUgaVMJ7BkQisyoRV1hYrAUkMoAksRWIrAUhSBpQgsRWD5RSa0MilgTXMTmv/v9k5b99d3zlRmZHTkvex1VIMVlONmzacA17KOio21dOycMD3pQ3eTXwz0lHXeyWzI4tftZfFjZmxiqi42Z0f2qKAhlZeX+81EbRoYGEAe7OrqElhvIYJKiquE1YTShMrMI9OoWjyOC5Z1ne0jRhRd8PHjx9GbUDpxUex8BClec/HixXbJoqIiPGnMuxyPzWzt4aCS+zCv+llNen3G/qoDPYM9KfdSe929p+szTj5I63W/LOkoOV13pvb5/cyGc8fvnyxoLRweGb788Epnf+e5pgssXNlVNWrGeDajPjO7JSeEQ8If2rgIsdoqAzewv7jg9fX1S5YswZxDm6PF7CnE/iLG4cnRArhfUQpWbW0terilh7EuUlJSsOFoEY496rNdBj72eTJ//nzztvGMIc0j6qbb7T5y5Ah9H/NdLldcXBzz8en8lme7HIBUnNIg0+hqutScDSKZjVnFT24WtBV29HdUP7sLK9cfF4NLRVely+3aVbFn1IxuLY0bHB3cU7mvsacp7cGpvqE+pmu677Lu4Mjg9rIdIRySkydPMuYFxiXmUnJyMpIqEjnnHnhhYmI/AxnWIY4hDcUOcgrBWXp6epSCxamWn5/PBDIqFwrx8fGHPKG93tUVWtfZFxc0c1uHaFzmdHZ2ckL7LWMfaXE8fY5KsC3iHnFvK4271JINTFtKtz3qbb3RfutMQxb9XeHja4D1sPdRn7vvQNVBFo6/s9MLVlbjebrFXeV76EZzH+XbZ0M4JCjzEMObZ6AA6hZ4UZ4pSL5gARMNiLXLApQxNOjm5uboAouOyRYhKjxIQRJnJPMxnlGcORHp494FFhix4ueff/4usJhGfabysRXvMrGxsdQzLkToO0JrFKpR3Yv6V0Ov1havh5XKp5V7q/YlViXR5QUCFs/urkg8cT8thIqFzG3bh3OPRgCdzZ6wv3R8aZ4wTQ8YExPDYkzTqrQSlxZR/akwWE93OADD2O81WYU5IBvW0CBvZ3RsNPCF6QRvdty63VF6rPZ4BD5y/qN9O9XUz8O2722SboM9eWBNpQkd2cAWVL3H5qbglbTfHhgZ0PdYiiKwFIGlCCxFEViKwFIElqIILGWagyUTWpEJragrVASWGkIRWIrAUgSWoggsRWApAssvMqGVSQFLJnSAkQn9HsCSCR1CZEIHB5ZM6EAiEzo4sGRCBxiZ0MGBJRM6wMiEDigyoYOKTOhQP0DJhA6+xWRCT5fIhJ7WYCmKwFIEliKwFEVgKdMMLL5hUkMokQ1QOWDNlL+ZKzMi4OSANTQ0JLaUyFLFl4izjOcbxe7ubv709lRRwggIAZL9avq/0p2LbK71A+cAAAAASUVORK5CYII=", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAOPElEQVR42u2deVsTVx+G/Xb9ALVeffuHtmql2kVrVWytWltr64KoqIgbolRFRMQNtCwioiLIqii7grIoShARwnbee3Js3pgibzYQzPNcXLmGyUwmc+ae35kE7jOzjDHDw8MvFCVCGRoaAqpZlqqxsTGjKGEHkMAJqGaJKmUy2HLAUlsokY0DVk9PjxpCiWyASmApAksRWIrAEliKwFI+DLD4pst3gbKyss7OTjWcEi5Yubm5X3zxxejoqP01ISGhtLRUDaeEC9ZPP/30xx9/UKj8wBoYGLh9+3Z5ebllrrW1taOj49atW263u6mpqaWl5ebNm/39/c+ePbt+/bq3yD1//vzatWsNDQ1q+qgG6/Hjx+vWrauvr9+8ebMvWPyVccWKFZmZmXv37k1MTGT+rl27li1bduLECfD67LPPjh07lpyc/N133+3YsSMrK2vhwoUvX77s7u5mzvnz59evX3/lyhW1fvSCBR95eXlMAERvb68XLMoSzLW3t9+9e3fx4sUWrJKSEiYAKzY21q4+b948ChsT27Ztq6mpoZKtXLmSFYHs0aNHav0oBYs+jquruLi4PXv2fPPNN1QaL1ivX7/++eefjx49mpaWFhMTY8G6c+eOBWv16tX2FebPn287SupWZWUlE3SdW7duXbVqFVVQrR+lYAEQPWCnJw8ePKCn84IFJfBh+8pFixYFCFZtbS0XXvxaUVFBDVPrRylYf/75p/ea3V7FNzY2WrC4Kl++fDld3u+///7pp59CTyBg0ZlydbVp0yYY9X1lJRo/FU4Qe/0UbAYHB71fXigCS1EEliKwFIGlKAJLEViKwFIUgaUILEVgKYrAUgSWIrAURWApAksRWIoisBSBpXz4YO3evXvleEHameJ3WVBQoEP14YAFQ0HNn7xM/RZHRkaCWp4RLt7j1qMOLLyd77///qOPPlq6dOmpU6dmBFjV1dWYkrzt1NTUwNc6c+YMCi7jXIQv4m7YsIH9xRB++vQptrDfC+JjFhUVqWI5WbBgAboYLYLyigN98ODBnJwchDCU1ydPnvT19WEvopG5XC6/FfGt/bZoBxCf1Ozfv/+vv/7CI2JkAEb/PXLkCO8TrRIpnIYCHexIhp9g5qVLl3DdEHoZZODcuXNYcXPnzv31119Pnz798OHDGzduWDU8qKAwffzxx69evUIc5xVQyeGMRkPDZAKLk+3SnnV1dbxJe7lCecO941lWiUawkKcZwYEDk5KSQnNw2VRYWMhR2bdvH3MOHTrEkfNdC4eR1gQ77xZpbpZn/qS2CEeRQVC+/vpryi2jnvz444/FxcWIkOCSnZ3NBOOaoFJyFBmi4t69exRj9o63SqnjbGEt9vHw4cPIlZw2IbyBq1evfvXVV7wUkO3cuZMXbGtr4zWTkpIOHDgA0zxCGJqn8QxiANw4w7wf63VGHVhM/Pbbb19++SWNAliczRwJ5lCrOOE44/99wgEfvjWaK1u0VE3BUCKXL19mKGnomTNnDiQx9gnvLT8/n5n0j2vXrmX8nNmzZzOTksZ7pkQZz+AUXrA4hehM6ctC2DrbZR+plCDFJixYbJ2CRO2kNFqwQJZhCsw/o2Pgl9OMdAVRChb7D1i2gHOQOC/Bi2PDYaDCc2D+vS6tDE9scWqoMp6BBRiogi1u374dpildHFR7gYj8zTFmgpJGwaAHZzwmX7BYzDYOJ0xo15T0a7/88gutQXVvbm5OT0/nBRkcyl72bdmyhSsKOlwuvBhWg01/8sknjOFD1WQV2zlG3adCPjfR2XGRa8ECKe8nKS5oJvgcRN1ii1M87JHvxdy4n/je9TGQFbno5g1T2CKydTvtuzk7x6/RpuDqM2Jg8X3VuN9jMSZWCFuiaFHbbVtQpWj9wNdl/KMZ9EmboZ0Y9UTfY+mbd0VgKQJLEVgCSxFYisBSBJaiCCxlRoDFXzF143UlsgEqVSxFXaEisBSBJbAUgaUILEVgKYrAUgSWIrD8M30Ue+WDAmv6KPbTKSNmbNLviee1JDAD8N6CXZ07vX/gYCFB4KOiK/GI/BmpN42CPIlN0pRsbi14M125xtRufuvZzlzTM7nCMboi5hl3IcXIOHv2bAh+M45hVFQsa95ZICAM/47GQn1GRqXhsFXxQnkP9LDcvhUP2C6JnFnjCQtwO9aMjAy6Zqxz5EScO3xo5hiPeIi7zKuxOiIQE2zCaoyhg1U027xsMO4eU/wfB6zhl6Z2m6neYPrbTFehcVWb/sfm3iZzP84M95vWDHNvc6RoQ1XFSbRF6+LFixYs7EUkVSsqImTTSjQCFj86K24c06iqqNKsgi3NNI5hFIGFvLtx40YGQUD1ZKwLWoQ/gNtGRNFk2lpl69atwyjn9r4suWbNmlxPcOiQNquqqhDevWekfQQjjE2W5JDQprwswOGZhQVWfaKpTzCt6c4EYIERhaphv2lIMi1HTcffpiLWmfnkvGlJNTUbTddVp4uM0PFAp/b+asFir2kNWg+J3LYDbcgjd3oHph9++IGhHDC2UTVh0SqvUQQWDUR5t2cYYFF7qDSccxYRwKLYGI9DTJMhGbMkjQhVLE8FYj53leaW0l6kLJQWLDuHtRhuhBU5m8MC60mOKVvm/DwvdcDqLDA1Gxyk6ve+Aaskxgy5nPnQBli9dRG8ukK89qtYGNgFnrS3t9t2AB1aiUFWaBkMafssbcUwE1HXFdJSmOmMlnHhwoVxwUJsT/IE69cuSctasBgNgTOSzpFlWJ5SzzlKj0CRYzwML1gM0QG7LBbWWEKA1Z5jWk+ZugTzvNwBq7vEIenOcqfLs2B15pmKFQ55/a2RBctWKXYNm55RGyxY6OPsFDuL8m/b4dtvv7VgsTy/UqqxzBlAhaEl6DSp31H3qfBdZrqtWL7P+i3pa5SPeGLG88ontvXDuPbxvJnhAefSqqvozcfDSQv77nc/9omHI/A+y/XANBm0beoU+4lDSecyYrp/1eB+YVrPvIFMCRksRRFYisBSBJaiCCxFYCkCS1EEljKdwJJir0ixV9QVKgJLDaEILEVgKQJLUQSWIrAUgeUXmdDKpIAV1Sb02/9y7vzq+7/kE9zDDQs5HKvRE5nQ/ycz0oTu7jbcHHX1ahTH/8HETTrz8kxdncGvev3aJCW9c3V8obqwpB2Z0IFmhpnQKSkmJ8eZ4IalbW0mPt6sXeuAVVho0GUXLjS4ZYcPO8whleAc44Bw7+dNm5wf7mAdHlgyoYMGa8aY0Bs3Gt8iMWcOHZLJzXWAKyhwkHrxwixdyj3BTUKC89TZs6a21sGO3fz77zDBkgkdNFgzxoQGHTAimZlOKZo715n+N1gXL5rUVKduUdWYmZhotmyh0oYJlkzooMGaMSY0RWjJEqeP4wdufMGi15s3z+EGsFwu51KMprhwgf7JmV60yNBbhX2NJRM6lMwYE3pwcPz5brff/vhPRCIyoSOWmWFCKxEBS1EEliKwFIGlKAJLEViKwFIUgaVMJ7BkQisyoRV1hYrAUkMoAksRWIrAUhSBpQgsRWD5RSa0MilgTXMTmv/v9k5b99d3zlRmZHTkvex1VIMVlONmzacA17KOio21dOycMD3pQ3eTXwz0lHXeyWzI4tftZfFjZmxiqi42Z0f2qKAhlZeX+81EbRoYGEAe7OrqElhvIYJKiquE1YTShMrMI9OoWjyOC5Z1ne0jRhRd8PHjx9GbUDpxUex8BClec/HixXbJoqIiPGnMuxyPzWzt4aCS+zCv+llNen3G/qoDPYM9KfdSe929p+szTj5I63W/LOkoOV13pvb5/cyGc8fvnyxoLRweGb788Epnf+e5pgssXNlVNWrGeDajPjO7JSeEQ8If2rgIsdoqAzewv7jg9fX1S5YswZxDm6PF7CnE/iLG4cnRArhfUQpWbW0terilh7EuUlJSsOFoEY496rNdBj72eTJ//nzztvGMIc0j6qbb7T5y5Ah9H/NdLldcXBzz8en8lme7HIBUnNIg0+hqutScDSKZjVnFT24WtBV29HdUP7sLK9cfF4NLRVely+3aVbFn1IxuLY0bHB3cU7mvsacp7cGpvqE+pmu677Lu4Mjg9rIdIRySkydPMuYFxiXmUnJyMpIqEjnnHnhhYmI/AxnWIY4hDcUOcgrBWXp6epSCxamWn5/PBDIqFwrx8fGHPKG93tUVWtfZFxc0c1uHaFzmdHZ2ckL7LWMfaXE8fY5KsC3iHnFvK4271JINTFtKtz3qbb3RfutMQxb9XeHja4D1sPdRn7vvQNVBFo6/s9MLVlbjebrFXeV76EZzH+XbZ0M4JCjzEMObZ6AA6hZ4UZ4pSL5gARMNiLXLApQxNOjm5uboAouOyRYhKjxIQRJnJPMxnlGcORHp494FFhix4ueff/4usJhGfabysRXvMrGxsdQzLkToO0JrFKpR3Yv6V0Ov1havh5XKp5V7q/YlViXR5QUCFs/urkg8cT8thIqFzG3bh3OPRgCdzZ6wv3R8aZ4wTQ8YExPDYkzTqrQSlxZR/akwWE93OADD2O81WYU5IBvW0CBvZ3RsNPCF6QRvdty63VF6rPZ4BD5y/qN9O9XUz8O2722SboM9eWBNpQkd2cAWVL3H5qbglbTfHhgZ0PdYiiKwFIGlCCxFEViKwFIElqIILGWagyUTWpEJragrVASWGkIRWIrAUgSWoggsRWApAssvMqGVSQFLJnSAkQn9HsCSCR1CZEIHB5ZM6EAiEzo4sGRCBxiZ0MGBJRM6wMiEDigyoYOKTOhQP0DJhA6+xWRCT5fIhJ7WYCmKwFIEliKwFEVgKdMMLL5hUkMokQ1QOWDNlL+ZKzMi4OSANTQ0JLaUyFLFl4izjOcbxe7ubv709lRRwggIAZL9avq/0p2LbK71A+cAAAAASUVORK5CYII=",
"description": "Visualization of alarms for devices, assets and other entities." "description": "Visualization of alarms for devices, assets and other entities.",
"externalId": null,
"name": "Alarm widgets"
}, },
"widgetTypes": [ "widgetTypes": [
{ {
@ -23,7 +25,9 @@
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings", "settingsDirective": "tb-alarms-table-widget-settings",
"dataKeySettingsDirective": "tb-alarms-table-key-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" "hasBasicMode": true,
"basicModeDirective": "tb-alarms-table-basic-config",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}"
} }
} }
] ]

46
application/src/main/data/json/system/widget_bundles/cards.json

@ -64,7 +64,9 @@
"settingsDirective": "tb-timeseries-table-widget-settings", "settingsDirective": "tb-timeseries-table-widget-settings",
"dataKeySettingsDirective": "tb-timeseries-table-key-settings", "dataKeySettingsDirective": "tb-timeseries-table-key-settings",
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true}" "hasBasicMode": true,
"basicModeDirective": "tb-timeseries-table-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}"
} }
}, },
{ {
@ -223,6 +225,48 @@
"settingsDirective": "tb-dashboard-state-widget-settings", "settingsDirective": "tb-dashboard-state-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}" "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
} }
},
{
"alias": "value_card",
"name": "Value card",
"image": null,
"description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.",
"descriptor": {
"type": "latest",
"sizeX": 2.5,
"sizeY": 2.5,
"resources": [],
"templateHtml": "<tb-value-card-widget \n [ctx]=\"ctx\">\n</tb-value-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px'\n };\n};\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-value-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"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\":{\"labelPosition\":\"top\"},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}
},
{
"alias": "horizontal_value_card",
"name": "Horizontal value card",
"image": null,
"description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.",
"descriptor": {
"type": "latest",
"sizeX": 5,
"sizeY": 1.5,
"resources": [],
"templateHtml": "<tb-value-card-widget \n [ctx]=\"ctx\">\n</tb-value-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '130px'\n };\n};\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-value-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"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\":{\"labelPosition\":\"top\"},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}
} }
] ]
} }

12
application/src/main/data/json/system/widget_bundles/charts.json

@ -161,7 +161,9 @@
"settingsDirective": "tb-flot-line-widget-settings", "settingsDirective": "tb-flot-line-widget-settings",
"dataKeySettingsDirective": "tb-flot-line-key-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" "hasBasicMode": true,
"basicModeDirective": "tb-flot-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}"
} }
}, },
{ {
@ -183,7 +185,9 @@
"settingsDirective": "tb-flot-line-widget-settings", "settingsDirective": "tb-flot-line-widget-settings",
"dataKeySettingsDirective": "tb-flot-line-key-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" "hasBasicMode": true,
"basicModeDirective": "tb-flot-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}"
} }
}, },
{ {
@ -204,7 +208,9 @@
"settingsDirective": "tb-flot-bar-widget-settings", "settingsDirective": "tb-flot-bar-widget-settings",
"dataKeySettingsDirective": "tb-flot-bar-key-settings", "dataKeySettingsDirective": "tb-flot-bar-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" "hasBasicMode": true,
"basicModeDirective": "tb-flot-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}"
} }
} }
] ]

2
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -498,7 +498,7 @@
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}", "dataKeySettingsSchema": "{}",
"settingsDirective": "tb-update-json-attribute-widget-settings", "settingsDirective": "tb-update-json-attribute-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetMode\":\"ATTRIBUTE\",\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
} }
} }
] ]

63
application/src/main/data/upgrade/3.5.1/schema_update.sql

@ -53,6 +53,69 @@ $$;
-- NOTIFICATION CONFIGS VERSION CONTROL END -- NOTIFICATION CONFIGS VERSION CONTROL END
-- EDGE EVENTS MIGRATION START
DO
$$
DECLARE table_partition RECORD;
BEGIN
-- in case of running the upgrade script a second time:
IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN
ALTER TABLE edge_event RENAME TO old_edge_event;
CREATE INDEX IF NOT EXISTS idx_old_edge_event_created_time_tmp ON old_edge_event(created_time);
ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time;
FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
FROM pg_tables WHERE tablename LIKE 'edge_event_%'
LOOP
EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts);
END LOOP;
ELSE
RAISE NOTICE 'Table old_edge_event already exists, leaving as is';
END IF;
END;
$$;
CREATE TABLE IF NOT EXISTS edge_event (
seq_id INT GENERATED ALWAYS AS IDENTITY,
id uuid NOT NULL,
created_time bigint NOT NULL,
edge_id uuid,
edge_event_type varchar(255),
edge_event_uid varchar(255),
entity_id uuid,
edge_event_action varchar(255),
body varchar(10000000),
tenant_id uuid,
ts bigint NOT NULL
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id);
ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
LANGUAGE plpgsql AS
$$
DECLARE
p RECORD;
partition_end_ts BIGINT;
BEGIN
FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms
LOOP
partition_end_ts = p.partition_ts + partition_size_ms;
RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts;
EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' ||
'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts);
END LOOP;
INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts)
SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts
FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms;
END;
$$;
-- EDGE EVENTS MIGRATION END
ALTER TABLE resource ALTER TABLE resource
ADD COLUMN IF NOT EXISTS etag varchar; ADD COLUMN IF NOT EXISTS etag varchar;

12
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -73,10 +73,14 @@ public class AppActor extends ContextAwareActor {
@Override @Override
protected boolean doProcess(TbActorMsg msg) { protected boolean doProcess(TbActorMsg msg) {
if (!ruleChainsInitialized) { if (!ruleChainsInitialized) {
initTenantActors(); if (MsgType.APP_INIT_MSG.equals(msg.getMsgType())) {
ruleChainsInitialized = true; initTenantActors();
if (msg.getMsgType() != MsgType.APP_INIT_MSG && msg.getMsgType() != MsgType.PARTITION_CHANGE_MSG) { ruleChainsInitialized = true;
log.warn("Rule Chains initialized by unexpected message: {}", msg); } else {
if (!msg.getMsgType().isIgnoreOnStart()) {
log.warn("Attempt to initialize Rule Chains by unexpected message: {}", msg);
}
return true;
} }
} }
switch (msg.getMsgType()) { switch (msg.getMsgType()) {

30
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -19,9 +19,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.ObjectPostProcessor;
@ -29,7 +31,6 @@ import org.springframework.security.config.annotation.authentication.builders.Au
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
@ -37,9 +38,11 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -119,6 +122,17 @@ public class ThingsboardSecurityConfiguration {
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
@Bean
protected FilterRegistrationBean<ShallowEtagHeaderFilter> buildEtagFilter() throws Exception {
ShallowEtagHeaderFilter etagFilter = new ShallowEtagHeaderFilter();
etagFilter.setWriteWeakETag(true);
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean
= new FilterRegistrationBean<>( etagFilter);
filterRegistrationBean.addUrlPatterns("*.js","*.css","*.ico","/assets/*","/static/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}
@Bean @Bean
protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception { protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler); RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler);
@ -181,8 +195,18 @@ public class ThingsboardSecurityConfiguration {
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
@Bean @Bean
public WebSecurityCustomizer webSecurityCustomizer() { @Order(0)
return (web) -> web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); SecurityFilterChain resources(HttpSecurity http) throws Exception {
http
.requestMatchers((matchers) -> matchers.antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"))
.headers().defaultsDisabled()
.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public"))
.and()
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
} }
@Bean @Bean

253
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -207,42 +207,222 @@ public class ControllerConstants {
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. "; protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION = protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
"{\n" + "{\n" +
" \"device\": {\n" + " \"device\": {\n" +
" \"name\": \"LwRpk00000000\",\n" + " \"name\":\"Name_DeviceWithCredantial_AccessToken\",\n" +
" \"type\": \"lwm2mProfileRpk\"\n" + " \"label\":\"Label_DeviceWithCredantial_AccessToken\",\n" +
" },\n" + " \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" + " \"credentials\": {\n" +
" \"id\": \"null\",\n" + " \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"createdTime\": 0,\n" + " \"credentialsId\": \"6hmxew8pmmzng4e3une2\"\n" +
" \"deviceId\": \"null\",\n" + " }\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\": {\n" +
" \"client\": {\n" +
" \"endpoint\": \"LwRpk00000000\",\n" +
" \"securityConfigClientMode\": \"RPK\",\n" +
" \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" +
" },\n" +
" \"bootstrap\": {\n" +
" \"bootstrapServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" },\n" +
" \"lwm2mServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}"; "}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN = protected static final String DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; "{\n" +
" \"id\": {\n" +
" \"id\":\"c886a090-168d-11ee-87c9-6f157dbc816a\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"c5fb3ac0-168d-11ee-87c9-6f157dbc816a\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une4\"\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_AccessToken_Default\",\n" +
" \"label\":\"Label_DeviceWithCredantial_AccessToken_Default\",\n" +
" \"type\": \"default\"\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une3\"\n" +
" }\n" +
"}";
protected static final String certificateValue = "\"-----BEGIN CERTIFICATE----- " +
"MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1owbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qKQiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaCkQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8GA1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZuccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== " +
"-----END CERTIFICATE-----\"";
protected static final String certificateId = "\"84f5911765abba1f96bf4165604e9e90338fc6214081a8e623b6ff9669aedb27\"";
protected static final String certificateValueUpdate = "\"-----BEGIN CERTIFICATE----- " +
"MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlowbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72krDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMyLydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8GA1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZNGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A== " +
"-----END CERTIFICATE-----\"";
protected static final String certificateIdUpdate = "\"6b8adb49015500e51a527acd332b51684ab9b49b4ade03a9582a44c455e2e9b6\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_X509_Certificate\",\n" +
" \"label\":\"Label_DeviceWithCredantial_X509_Certificate\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"X509_CERTIFICATE\",\n" +
" \"credentialsId\": " + certificateId + ",\n" +
" \"credentialsValue\": " + certificateValue + "\n" +
" }\n" +
"}";
protected static final String DEVICE_UPDATE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION =
"{\n" +
" \"id\": {\n" +
" \"id\":\"309bd9c0-14f4-11ee-9fc9-d9b7463abb63\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"3092b200-14f4-11ee-9fc9-d9b7463abb63\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"X509_CERTIFICATE\",\n" +
" \"credentialsId\": " + certificateIdUpdate + ",\n" +
" \"credentialsValue\": " + certificateValueUpdate + "\n" +
"}";
protected static final String MQTT_BASIC_VALUE = "\"{\\\"clientId\\\":\\\"5euh5nzm34bjjh1efmlt\\\",\\\"userName\\\":\\\"onasd1lgwasmjl7v2v7h\\\",\\\"password\\\":\\\"b9xtm4ny8kt9zewaga5o\\\"}\"";
protected static final String MQTT_BASIC_VALUE_UPDATE = "\"{\\\"clientId\\\":\\\"juy03yv4owqxcmqhqtvk\\\",\\\"userName\\\":\\\"ov19fxca0cyjn7lm7w7u\\\",\\\"password\\\":\\\"twy94he114dfi9usyk1o\\\"}\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_MQTT_Basic\",\n" +
" \"label\":\"Label_DeviceWithCredantial_MQTT_Basic\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"MQTT_BASIC\",\n" +
" \"credentialsValue\": " + MQTT_BASIC_VALUE + "\n" +
" }\n" +
"}";
protected static final String DEVICE_UPDATE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION =
"{\n" +
" \"id\": {\n" +
" \"id\":\"d877ffb0-14f5-11ee-9fc9-d9b7463abb63\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"d875dcd0-14f5-11ee-9fc9-d9b7463abb63\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"MQTT_BASIC\",\n" +
" \"credentialsValue\": " + MQTT_BASIC_VALUE_UPDATE + "\n" +
"}";
protected static final String CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION =
" \"{" +
"\\\"client\\\":{ " +
"\\\"endpoint\\\":\\\"LwRpk00000000\\\", " +
"\\\"securityConfigClientMode\\\":\\\"RPK\\\", " +
"\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\"" +
" }, " +
"\\\"bootstrap\\\":{ " +
"\\\"bootstrapServer\\\":{ " +
"\\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}, " +
"\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}" +
"} " +
"}\"";
protected static final String CREDENTIALS_VALUE_UPDATE_LVM2M_RPK_DESCRIPTION =
" \"{" +
"\\\"client\\\":{ " +
"\\\"endpoint\\\":\\\"LwRpk00000000\\\", " +
"\\\"securityConfigClientMode\\\":\\\"RPK\\\", " +
"\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdvBZZ2vQRK9wgDhctj6B1c7bxR3Z0wYg1+YdoYFnVUKWb+rIfTTyYK9tmQJx5Vlb5fxdLnVv1RJOPiwsLIQbAA==\\\"" +
" }, " +
"\\\"bootstrap\\\":{ " +
"\\\"bootstrapServer\\\":{ " +
"\\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}, " +
"\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}" +
"} " +
"}\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_LwRpk00000000\",\n" +
" \"label\":\"Label_LwRpk00000000\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"a660bd50-10ef-11ee-8737-b5634e73c779\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\":\n" + CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION + "\n" +
" }\n" +
"}";
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION =
"{\n" +
" \"id\": {\n" +
" \"id\":\"e238d4d0-1689-11ee-98c6-1713c1be5a8e\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"e232e160-1689-11ee-98c6-1713c1be5a8e\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\":\n" + CREDENTIALS_VALUE_UPDATE_LVM2M_RPK_DESCRIPTION + "\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE + protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
@ -254,7 +434,7 @@ public class ControllerConstants {
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" + " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n"; " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"schedule\":{\n" + " \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" + " \"type\":\"SPECIFIC_TIME\",\n" +
@ -269,7 +449,7 @@ public class ControllerConstants {
" }\n" + " }\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END; MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"schedule\":{\n" + " \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" + " \"type\":\"CUSTOM\",\n" +
@ -321,9 +501,9 @@ public class ControllerConstants {
" }\n" + " }\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END; MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"spec\":{\n" + " \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" + " \"type\":\"REPEATING\",\n" +
@ -339,7 +519,8 @@ public class ControllerConstants {
" }\n" + " }\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END; MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"spec\":{\n" + " \"spec\":{\n" +
" \"type\":\"DURATION\",\n" + " \"type\":\"DURATION\",\n" +

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

@ -75,6 +75,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.validation.Valid;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -93,7 +94,15 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
@ -182,18 +191,29 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Create Device (saveDevice) with credentials ", @ApiOperation(value = "Create Device (saveDevice) with credentials ",
notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK +
"Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" +
"You may find the example of LwM2M device and RPK credentials below: \n\n" + "You may find the example of device with different type of credentials below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN + "- Credentials type: <b>\"Access token\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile default</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Device saveDeviceWithCredentials(@ApiParam(value = "The JSON object with device and credentials. See method description above for example.") public Device saveDeviceWithCredentials(@ApiParam(value = "The JSON object with device and credentials. See method description above for example.")
@RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException {
Device device = checkNotNull(deviceAndCredentials.getDevice()); Device device = deviceAndCredentials.getDevice();
DeviceCredentials credentials = checkNotNull(deviceAndCredentials.getCredentials()); DeviceCredentials credentials = deviceAndCredentials.getCredentials();
device.setTenantId(getCurrentUser().getTenantId()); device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE); checkEntity(device.getId(), device, Resource.DEVICE);
return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser()); return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser());
@ -277,10 +297,27 @@ public class DeviceController extends BaseController {
return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser()); return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser());
} }
@ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " + @ApiOperation(value = "Update device credentials (updateDeviceCredentials)",
"Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value. " + notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. \" +\n" +
"Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device. " + "Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value.\n" +
"The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'." + TENANT_AUTHORITY_PARAGRAPH) "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" +
"The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" +
"You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device ID</b> and with <b>device ID</b> below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Update to real value:\n" +
" - 'id' (this is id of Device Credentials -> \"Get Device Credentials (getDeviceCredentialsByDeviceId)\",\n" +
" - 'deviceId.id' (this is id of Device).\n" +
"Remove 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity." +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/credentials", method = RequestMethod.POST) @RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
@ResponseBody @ResponseBody

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

@ -85,6 +85,6 @@ public class EdgeEventController extends BaseController {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
checkEdgeId(edgeId, Operation.READ); checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink));
} }
} }

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -341,7 +341,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
sessionNewEvents.put(edgeId, false); sessionNewEvents.put(edgeId, false);
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override @Override
public void onSuccess(Void result) { public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) {
sessionNewEvents.put(edgeId, true);
}
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);
} }

237
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -24,6 +24,7 @@ import io.grpc.stub.StreamObserver;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.data.util.Pair;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -35,6 +36,8 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
@ -68,17 +71,15 @@ import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher;
import java.io.Closeable; import java.io.Closeable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Data @Data
@ -89,6 +90,7 @@ public final class EdgeGrpcSession implements Closeable {
private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private final UUID sessionId; private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener; private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
@ -103,6 +105,12 @@ public final class EdgeGrpcSession implements Closeable {
private boolean connected; private boolean connected;
private boolean syncCompleted; private boolean syncCompleted;
private Long newStartTs;
private Long previousStartTs;
private Long newStartSeqId;
private Long previousStartSeqId;
private Long seqIdEnd;
private EdgeVersion edgeVersion; private EdgeVersion edgeVersion;
private int maxInboundMessageSize; private int maxInboundMessageSize;
@ -204,10 +212,10 @@ public final class EdgeGrpcSession implements Closeable {
EdgeEventFetcher next = cursor.getNext(); EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
ListenableFuture<UUID> uuidListenableFuture = startProcessingEdgeEvents(next); ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next);
Futures.addCallback(uuidListenableFuture, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable UUID result) { public void onSuccess(@Nullable Pair<Long, Long> result) {
doSync(cursor); doSync(cursor);
} }
@ -307,36 +315,51 @@ public final class EdgeGrpcSession implements Closeable {
sendDownlinkMsg(edgeConfigMsg); sendDownlinkMsg(edgeConfigMsg);
} }
ListenableFuture<Void> processEdgeEvents() throws Exception { ListenableFuture<Boolean> processEdgeEvents() throws Exception {
SettableFuture<Void> result = SettableFuture.create(); SettableFuture<Boolean> result = SettableFuture.create();
log.trace("[{}] starting processing edge events", this.sessionId); log.trace("[{}] starting processing edge events", this.sessionId);
if (isConnected() && isSyncCompleted()) { if (isConnected() && isSyncCompleted()) {
Long queueStartTs = getQueueStartTs().get(); Pair<Long, Long> startTsAndSeqId = getQueueStartTsAndSeqId().get();
this.previousStartTs = startTsAndSeqId.getFirst();
this.previousStartSeqId = startTsAndSeqId.getSecond();
GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher( GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher(
queueStartTs, this.previousStartTs,
this.previousStartSeqId,
this.seqIdEnd,
false,
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService()); ctx.getEdgeEventService());
ListenableFuture<UUID> ifOffsetFuture = startProcessingEdgeEvents(fetcher); Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
Futures.addCallback(ifOffsetFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable UUID ifOffset) { public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
if (ifOffset != null) { if (newStartTsAndSeqId != null) {
Long newStartTs = Uuids.unixTimestamp(ifOffset); ListenableFuture<List<String>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
ListenableFuture<List<String>> updateFuture = updateQueueStartTs(newStartTs);
Futures.addCallback(updateFuture, new FutureCallback<>() { Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable List<String> list) { public void onSuccess(@Nullable List<String> list) {
log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs); log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId);
result.set(null); if (fetcher.isSeqIdNewCycleStarted()) {
seqIdEnd = fetcher.getSeqIdEnd();
boolean newEventsAvailable = isNewEdgeEventsAvailable();
result.set(newEventsAvailable);
} else {
seqIdEnd = null;
boolean newEventsAvailable = isSeqIdStartedNewCycle();
if (!newEventsAvailable) {
newEventsAvailable = isNewEdgeEventsAvailable();
}
result.set(newEventsAvailable);
}
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}] Failed to update queue offset [{}]", sessionId, ifOffset, t); log.error("[{}] Failed to update queue offset [{}]", sessionId, newStartTsAndSeqId, t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}] ifOffset is null. Skipping iteration without db update", sessionId); log.trace("[{}] newStartTsAndSeqId is null. Skipping iteration without db update", sessionId);
result.set(null); result.set(null);
} }
} }
@ -354,14 +377,14 @@ public final class EdgeGrpcSession implements Closeable {
return result; return result;
} }
private ListenableFuture<UUID> startProcessingEdgeEvents(EdgeEventFetcher fetcher) { private ListenableFuture<Pair<Long, Long>> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
SettableFuture<UUID> result = SettableFuture.create(); SettableFuture<Pair<Long, Long>> result = SettableFuture.create();
PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount());
processEdgeEvents(fetcher, pageLink, result); processEdgeEvents(fetcher, pageLink, result);
return result; return result;
} }
private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<UUID> result) { private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) {
try { try {
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
@ -377,8 +400,15 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && pageData.hasNext()) { if (isConnected() && pageData.hasNext()) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result); processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else { } else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1);
result.set(ifOffset); UUID idOffset = latestEdgeEvent.getUuidId();
if (idOffset != null) {
Long newStartTs = Uuids.unixTimestamp(idOffset);
long newStartSeqId = latestEdgeEvent.getSeqId();
result.set(Pair.of(newStartTs, newStartSeqId));
} else {
result.set(null);
}
} }
} }
} }
@ -461,69 +491,113 @@ public final class EdgeGrpcSession implements Closeable {
} }
} }
private DownlinkMsg convertToDownlinkMsg(EdgeEvent edgeEvent) { private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent); List<DownlinkMsg> result = new ArrayList<>();
DownlinkMsg downlinkMsg = null; for (EdgeEvent edgeEvent : edgeEvents) {
try { log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
switch (edgeEvent.getAction()) { DownlinkMsg downlinkMsg = null;
case UPDATED: try {
case ADDED: switch (edgeEvent.getAction()) {
case DELETED: case UPDATED:
case ASSIGNED_TO_EDGE: case ADDED:
case UNASSIGNED_FROM_EDGE: case DELETED:
case ALARM_ACK: case ASSIGNED_TO_EDGE:
case ALARM_CLEAR: case UNASSIGNED_FROM_EDGE:
case CREDENTIALS_UPDATED: case ALARM_ACK:
case RELATION_ADD_OR_UPDATE: case ALARM_CLEAR:
case RELATION_DELETED: case CREDENTIALS_UPDATED:
case ASSIGNED_TO_CUSTOMER: case RELATION_ADD_OR_UPDATE:
case UNASSIGNED_FROM_CUSTOMER: case RELATION_DELETED:
case CREDENTIALS_REQUEST: case CREDENTIALS_REQUEST:
case RPC_CALL: case RPC_CALL:
downlinkMsg = convertEntityEventToDownlink(edgeEvent); case ASSIGNED_TO_CUSTOMER:
log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg); case UNASSIGNED_FROM_CUSTOMER:
break; downlinkMsg = convertEntityEventToDownlink(edgeEvent);
case ATTRIBUTES_UPDATED: log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
case POST_ATTRIBUTES: break;
case ATTRIBUTES_DELETED: case ATTRIBUTES_UPDATED:
case TIMESERIES_UPDATED: case POST_ATTRIBUTES:
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent); case ATTRIBUTES_DELETED:
break; case TIMESERIES_UPDATED:
default: downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction()); break;
default:
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
}
} catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e);
}
if (downlinkMsg != null) {
result.add(downlinkMsg);
}
}
return result;
}
private ListenableFuture<Pair<Long, Long>> getQueueStartTsAndSeqId() {
ListenableFuture<List<AttributeKvEntry>> future =
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY));
return Futures.transform(future, attributeKvEntries -> {
long startTs = 0L;
long startSeqId = 0L;
for (AttributeKvEntry attributeKvEntry : attributeKvEntries) {
if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) {
startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
}
if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) {
startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
}
}
if (startSeqId == 0L) {
startSeqId = findStartSeqIdFromOldestEventIfAny();
} }
return Pair.of(startTs, startSeqId);
}, ctx.getGrpcCallbackExecutorService());
}
private boolean isSeqIdStartedNewCycle() {
try {
TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, this.previousStartSeqId == 0 ? null : this.previousStartSeqId - 1, pageLink);
return !edgeEvents.getData().isEmpty();
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e); log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", edge.getTenantId(), edge.getId(), sessionId, e);
} }
return downlinkMsg; return false;
} }
private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) { private boolean isNewEdgeEventsAvailable() {
return edgeEvents try {
.stream() TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
.map(this::convertToDownlinkMsg) PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), this.newStartSeqId, null, pageLink);
.filter(Objects::nonNull) return !edgeEvents.getData().isEmpty();
.collect(Collectors.toList()); } catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e);
}
return false;
} }
private ListenableFuture<Long> getQueueStartTs() { private long findStartSeqIdFromOldestEventIfAny() {
ListenableFuture<Optional<AttributeKvEntry>> future = long startSeqId = 0L;
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY); try {
return Futures.transform(future, attributeKvEntryOpt -> { TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime"), null, null);
if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) { PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink);
AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get(); if (!edgeEvents.getData().isEmpty()) {
return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; startSeqId = edgeEvents.getData().get(0).getSeqId() - 1;
} else {
return 0L;
} }
}, ctx.getGrpcCallbackExecutorService()); } catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", edge.getTenantId(), edge.getId(), sessionId, e);
}
return startSeqId;
} }
private ListenableFuture<List<String>> updateQueueStartTs(Long newStartTs) { private ListenableFuture<List<String>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) {
log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs); this.newStartTs = pair.getFirst();
List<AttributeKvEntry> attributes = Collections.singletonList( this.newStartSeqId = pair.getSecond();
new BaseAttributeKvEntry( log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);
new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis())); List<AttributeKvEntry> attributes = Arrays.asList(
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, this.newStartTs), System.currentTimeMillis()),
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, this.newStartSeqId), System.currentTimeMillis()));
return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes); return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes);
} }
@ -693,8 +767,11 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void interruptPreviousSendDownlinkMsgsTask() { private void interruptPreviousSendDownlinkMsgsTask() {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()
stopCurrentSendDownlinkMsgsTask(true); || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
}
} }
private void interruptGeneralProcessingOnSync() { private void interruptGeneralProcessingOnSync() {

18
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java

@ -18,7 +18,10 @@ package org.thingsboard.server.service.edge.rpc.constructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
@ -47,13 +50,22 @@ public class AlarmMsgConstructor {
String entityName = null; String entityName = null;
switch (alarm.getOriginator().getEntityType()) { switch (alarm.getOriginator().getEntityType()) {
case DEVICE: case DEVICE:
entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName(); Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId()));
if (deviceById != null) {
entityName = deviceById.getName();
}
break; break;
case ASSET: case ASSET:
entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName(); Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId()));
if (assetById != null) {
entityName = assetById.getName();
}
break; break;
case ENTITY_VIEW: case ENTITY_VIEW:
entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName(); EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId()));
if (entityViewById != null) {
entityName = entityViewById.getName();
}
break; break;
} }
AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder()

35
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java

@ -16,19 +16,27 @@
package org.thingsboard.server.service.edge.rpc.fetch; package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class GeneralEdgeEventFetcher implements EdgeEventFetcher { public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
private final Long queueStartTs; private final Long queueStartTs;
private Long seqIdStart;
@Getter
private Long seqIdEnd;
@Getter
private boolean seqIdNewCycleStarted;
private Long maxReadRecordsCount;
private final EdgeEventService edgeEventService; private final EdgeEventService edgeEventService;
@Override @Override
@ -37,13 +45,32 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
pageSize, pageSize,
0, 0,
null, null,
new SortOrder("createdTime", SortOrder.Direction.ASC), null,
queueStartTs, queueStartTs,
null); System.currentTimeMillis());
} }
@Override @Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
return edgeEventService.findEdgeEvents(tenantId, edge.getId(), (TimePageLink) pageLink, true); try {
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().isEmpty()) {
this.seqIdEnd = Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount);
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
this.seqIdNewCycleStarted = true;
this.seqIdStart = 0L;
} else {
edgeEvents = new PageData<>();
log.warn("[{}] unexpected edge notification message received. " +
"no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId());
}
}
return edgeEvents;
} catch (Exception e) {
log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId());
}
return new PageData<>();
} }
} }

38
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -36,9 +37,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@ -210,6 +215,39 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
return alarmInfo; return alarmInfo;
} }
@Override
public void unassignUserAlarms(TenantId tenantId, User user, long unassignTs) {
AlarmQueryV2 alarmQuery = AlarmQueryV2.builder().assigneeId(user.getId()).pageLink(new TimePageLink(Integer.MAX_VALUE)).build();
try {
List<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, alarmQuery).get(30, TimeUnit.SECONDS).getData();
for (AlarmInfo alarm : alarms) {
AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarm.getId(), getOrDefault(unassignTs));
if (!result.isSuccessful()) {
continue;
}
if (result.isModified()) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned because user %s - was deleted",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ASSIGN"))
.build();
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user);
}
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
}
@Override @Override
public Boolean delete(Alarm alarm, User user) { public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId(); TenantId tenantId = alarm.getTenantId();

3
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
public interface TbAlarmService { public interface TbAlarmService {
@ -37,5 +38,7 @@ public interface TbAlarmService {
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
void unassignUserAlarms(TenantId tenantId, User user, long unassignTs);
Boolean delete(Alarm alarm, User user); Boolean delete(Alarm alarm, User user);
} }

3
application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -43,6 +44,7 @@ import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATT
public class DefaultUserService extends AbstractTbEntityService implements TbUserService { public class DefaultUserService extends AbstractTbEntityService implements TbUserService {
private final UserService userService; private final UserService userService;
private final TbAlarmService tbAlarmService;
private final MailService mailService; private final MailService mailService;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
@ -80,6 +82,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
UserId userId = tbUser.getId(); UserId userId = tbUser.getId();
try { try {
tbAlarmService.unassignUserAlarms(tenantId, tbUser, System.currentTimeMillis());
userService.deleteUser(tenantId, userId); userService.deleteUser(tenantId, userId);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser, notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser,
user, ActionType.DELETED, true, null, customerId.toString()); user, ActionType.DELETED, true, null, customerId.toString());

19
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -202,22 +202,27 @@ public class DefaultDataUpdateService implements DataUpdateService {
} else { } else {
log.info("Skipping audit logs migration"); log.info("Skipping audit logs migration");
} }
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); migrateEdgeEvents("Starting edge events migration. ");
if (!skipEdgeEventsMigration) {
log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
break; break;
case "3.5.1": case "3.5.1":
log.info("Updating data from version 3.5.1 to 3.5.2 ..."); log.info("Updating data from version 3.5.1 to 3.5.2 ...");
migrateEdgeEvents("Starting edge events migration - adding seq_id column. ");
break; break;
default: default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
} }
} }
private void migrateEdgeEvents(String logPrefix) {
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
if (!skipEdgeEventsMigration) {
log.info(logPrefix + "Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
}
@Override @Override
public void upgradeRuleNodes() { public void upgradeRuleNodes() {
try { try {

2
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -108,6 +108,8 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
.alarmOriginatorName(alarmInfo.getOriginatorName()) .alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity()) .alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus()) .alarmStatus(alarmInfo.getStatus())
.acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId()) .alarmCustomerId(alarmInfo.getCustomerId())
.build(); .build();
} }

16
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -259,7 +259,21 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
} }
void launchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { void launchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix)); if (isReady) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
} else {
scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
}
}
private void scheduleLaunchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
repartitionExecutor.schedule(() -> {
if (isReady) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
} else {
scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
}
}, 10, TimeUnit.SECONDS);
} }
void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {

3
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -68,7 +68,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected volatile ExecutorService consumersExecutor; protected volatile ExecutorService consumersExecutor;
protected volatile ExecutorService notificationsConsumerExecutor; protected volatile ExecutorService notificationsConsumerExecutor;
protected volatile boolean stopped = false; protected volatile boolean stopped = false;
protected volatile boolean isReady = false;
protected final ActorSystemContext actorContext; protected final ActorSystemContext actorContext;
protected final DataDecodingEncodingService encodingService; protected final DataDecodingEncodingService encodingService;
protected final TbTenantProfileCache tenantProfileCache; protected final TbTenantProfileCache tenantProfileCache;
@ -108,6 +108,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
public void onApplicationEvent(ApplicationReadyEvent event) { public void onApplicationEvent(ApplicationReadyEvent event) {
log.info("Subscribing to notifications: {}", nfConsumer.getTopic()); log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
this.nfConsumer.subscribe(); this.nfConsumer.subscribe();
this.isReady = true;
launchNotificationsConsumer(); launchNotificationsConsumer();
launchMainConsumers(); launchMainConsumers();
} }

8
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -27,14 +27,18 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -55,9 +59,11 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource
public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService { public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService {
private final ResourceService resourceService; private final ResourceService resourceService;
private final WidgetTypeService widgetTypeService;
public DefaultTbResourceService(ResourceService resourceService) { public DefaultTbResourceService(ResourceService resourceService, WidgetTypeService widgetTypeService) {
this.resourceService = resourceService; this.resourceService = resourceService;
this.widgetTypeService = widgetTypeService;
} }
@Override @Override

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

@ -497,6 +497,9 @@ cache:
entityCount: entityCount:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}" timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}"
maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}" maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}"
resourceInfo:
timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_INFO_TTL:1440}"
maxSize: "${CACHE_SPECS_RESOURCE_INFO_MAX_SIZE:100000}"
# deliberately placed outside 'specs' group above # deliberately placed outside 'specs' group above
notificationRules: notificationRules:

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

@ -32,6 +32,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -529,6 +531,51 @@ public class AlarmControllerTest extends AbstractControllerTest {
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test
public void testUnassignAlarmOnUserRemoving() throws Exception {
loginDifferentTenant();
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("tenantForAssign@thingsboard.org");
User savedUser = createUser(user, "password");
Device device = createDevice("Different tenant device", "default", "differentTenantTest");
Alarm alarm = Alarm.builder()
.type(TEST_ALARM_TYPE)
.tenantId(savedDifferentTenant.getId())
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(alarm);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
beforeAssignmentTs = System.currentTimeMillis();
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
}
@Test @Test
public void testFindAlarmsViaCustomerUser() throws Exception { public void testFindAlarmsViaCustomerUser() throws Exception {
loginCustomerUser(); loginCustomerUser();

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

@ -244,6 +244,53 @@ public class DeviceControllerTest extends AbstractControllerTest {
testNotificationUpdateGatewayOneTime(savedDevice, oldDevice); testNotificationUpdateGatewayOneTime(savedDevice, oldDevice);
} }
@Test
public void testSaveDeviceWithCredentials_CredentialsIsNull() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, null);
doPost("/api/device-with-credentials", saveRequest).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Validation error: credentials must not be null")));
}
@Test
public void testSaveDeviceWithCredentials_DeviceIsNull() throws Exception {
String testToken = "TEST_TOKEN";
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
deviceCredentials.setCredentialsId(testToken);
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(null, deviceCredentials);
doPost("/api/device-with-credentials", saveRequest).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Validation error: device must not be null")));
}
@Test
public void testSaveDeviceWithCredentials_WithExistingName() throws Exception {
String testToken = "TEST_TOKEN";
Device device = new Device();
device.setName("My device");
device.setType("default");
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
deviceCredentials.setCredentialsId(testToken);
SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials);
Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService);
Device savedDevice = readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class);
Assert.assertNotNull(savedDevice);
doPost("/api/device-with-credentials", saveRequest).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Device with such name already exists!")));
}
@Test @Test
public void saveDeviceWithViolationOfValidation() throws Exception { public void saveDeviceWithViolationOfValidation() throws Exception {
Device device = new Device(); Device device = new Device();

32
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -38,6 +38,8 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -216,6 +218,36 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr)))); .andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr))));
} }
@Test
public void testShoudNotDeleteTbResourceIfAssignedToWidgetType() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
Mockito.reset(tbClusterService, auditLogService);
String resourceIdStr = savedResource.getId().getId().toString();
//create widget type
WidgetsBundle widgetsBundle = new WidgetsBundle();
widgetsBundle.setTitle("My widgets bundle");
WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":{\"entityType\":\"TB_RESOURCE\",\"id\":\"%s\"},\"isModule\":true}]}", savedResource.getId()), JsonNode.class));
doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
doDelete("/api/resource/" + resourceIdStr)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Following widget types uses current resource: ["
+ widgetType .getName()+ "]")));
}
@Test @Test
public void testFindTenantTbResources() throws Exception { public void testFindTenantTbResources() throws Exception {

9
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -100,6 +100,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@TestPropertySource(properties = { @TestPropertySource(properties = {
"edges.enabled=true", "edges.enabled=true",
"queue.rule-engine.stats.enabled=false",
}) })
abstract public class AbstractEdgeTest extends AbstractControllerTest { abstract public class AbstractEdgeTest extends AbstractControllerTest {
@ -181,14 +182,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@After @After
public void afterTest() throws Exception { public void afterTest() throws Exception {
try {
edgeImitator.disconnect();
} catch (Exception ignored){}
loginSysAdmin(); loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getUuidId()) doDelete("/api/tenant/" + savedTenant.getUuidId())
.andExpect(status().isOk()); .andExpect(status().isOk());
try {
edgeImitator.disconnect();
} catch (Exception ignored) {}
} }
private void installation() { private void installation() {

4
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -94,8 +94,6 @@ public class EdgeImitator {
@Getter @Getter
private UplinkResponseMsg latestResponseMsg; private UplinkResponseMsg latestResponseMsg;
private boolean connected = false;
public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException { public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException {
edgeRpcClient = new EdgeGrpcClient(); edgeRpcClient = new EdgeGrpcClient();
messagesLatch = new CountDownLatch(0); messagesLatch = new CountDownLatch(0);
@ -120,7 +118,6 @@ public class EdgeImitator {
} }
public void connect() { public void connect() {
connected = true;
edgeRpcClient.connect(routingKey, routingSecret, edgeRpcClient.connect(routingKey, routingSecret,
this::onUplinkResponse, this::onUplinkResponse,
this::onEdgeUpdate, this::onEdgeUpdate,
@ -131,7 +128,6 @@ public class EdgeImitator {
} }
public void disconnect() throws InterruptedException { public void disconnect() throws InterruptedException {
connected = false;
edgeRpcClient.disconnect(false); edgeRpcClient.disconnect(false);
} }

1
application/src/test/resources/application-test.properties

@ -14,6 +14,7 @@ edges.enabled=false
edges.storage.no_read_records_sleep=500 edges.storage.no_read_records_sleep=500
edges.storage.sleep_between_batches=500 edges.storage.sleep_between_batches=500
actors.rpc.sequential=true actors.rpc.sequential=true
queue.rule-engine.stats.enabled=true
# Transports disabled to speed up the context init. Particular transport will be enabled with @TestPropertySource in respective tests # Transports disabled to speed up the context init. Particular transport will be enabled with @TestPropertySource in respective tests
transport.http.enabled=false transport.http.enabled=false

40
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java

@ -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.
*/
package org.thingsboard.server.cache.resourceInfo;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class ResourceInfoCacheKey implements Serializable {
private final TenantId tenantId;
private final TbResourceId tbResourceId;
@Override
public String toString() {
return tenantId + "_" + tbResourceId;
}
}

34
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java

@ -0,0 +1,34 @@
/**
* 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.cache.resourceInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.TbResourceInfo;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("ResourceInfoCache")
public class ResourceInfoCaffeineCache extends CaffeineTbTransactionalCache<ResourceInfoCacheKey, TbResourceInfo> {
public ResourceInfoCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.RESOURCE_INFO_CACHE);
}
}

26
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java

@ -0,0 +1,26 @@
/**
* 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.cache.resourceInfo;
import lombok.Data;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class ResourceInfoEvictEvent {
private final TenantId tenantId;
private final TbResourceId resourceId;
}

35
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java

@ -0,0 +1,35 @@
/**
* 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.cache.resourceInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbFSTRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.TbResourceInfo;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("ResourceInfoCache")
public class ResourceInfoRedisCache extends RedisTbTransactionalCache<ResourceInfoCacheKey, TbResourceInfo> {
public ResourceInfoRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.RESOURCE_INFO_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java

@ -26,7 +26,7 @@ public interface EdgeEventService {
ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent); ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent);
PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink);
/** /**
* Executes stored procedure to cleanup old edge events. * Executes stored procedure to cleanup old edge events.

3
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.widget; package org.thingsboard.server.dao.widget;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
@ -40,6 +41,8 @@ public interface WidgetTypeService extends EntityDaoService {
List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId);
WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias); WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias);
void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);

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

@ -44,4 +44,5 @@ public class CacheConstants {
public static final String USER_SETTINGS_CACHE = "userSettings"; public static final String USER_SETTINGS_CACHE = "userSettings";
public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles";
public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo";
} }

18
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -35,9 +35,9 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
@NoXss @NoXss
@Length(fieldName = "title") @Length(fieldName = "title")
@ApiModelProperty(position = 3, value = "Title of the customer", example = "Company A") @ApiModelProperty(position = 3, required = true, value = "Title of the customer", example = "Company A")
private String title; private String title;
@ApiModelProperty(position = 5, required = true, value = "JSON object with Tenant Id") @ApiModelProperty(position = 5, value = "JSON object with Tenant Id")
private TenantId tenantId; private TenantId tenantId;
@Getter @Setter @Getter @Setter
@ -89,43 +89,43 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
return super.getCreatedTime(); return super.getCreatedTime();
} }
@ApiModelProperty(position = 6, required = true, value = "Country", example = "US") @ApiModelProperty(position = 6, value = "Country", example = "US")
@Override @Override
public String getCountry() { public String getCountry() {
return super.getCountry(); return super.getCountry();
} }
@ApiModelProperty(position = 7, required = true, value = "State", example = "NY") @ApiModelProperty(position = 7, value = "State", example = "NY")
@Override @Override
public String getState() { public String getState() {
return super.getState(); return super.getState();
} }
@ApiModelProperty(position = 8, required = true, value = "City", example = "New York") @ApiModelProperty(position = 8, value = "City", example = "New York")
@Override @Override
public String getCity() { public String getCity() {
return super.getCity(); return super.getCity();
} }
@ApiModelProperty(position = 9, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400") @ApiModelProperty(position = 9, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override @Override
public String getAddress() { public String getAddress() {
return super.getAddress(); return super.getAddress();
} }
@ApiModelProperty(position = 10, required = true, value = "Address Line 2", example = "") @ApiModelProperty(position = 10, value = "Address Line 2", example = "")
@Override @Override
public String getAddress2() { public String getAddress2() {
return super.getAddress2(); return super.getAddress2();
} }
@ApiModelProperty(position = 11, required = true, value = "Zip code", example = "10004") @ApiModelProperty(position = 11, value = "Zip code", example = "10004")
@Override @Override
public String getZip() { public String getZip() {
return super.getZip(); return super.getZip();
} }
@ApiModelProperty(position = 12, required = true, value = "Phone number", example = "+1(415)777-7777") @ApiModelProperty(position = 12, value = "Phone number", example = "+1(415)777-7777")
@Override @Override
public String getPhone() { public String getPhone() {
return super.getPhone(); return super.getPhone();

2
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -85,7 +85,7 @@ public class DashboardInfo extends BaseData<DashboardId> implements HasName, Has
this.tenantId = tenantId; this.tenantId = tenantId;
} }
@ApiModelProperty(position = 4, value = "Title of the dashboard.") @ApiModelProperty(position = 4, required = true, value = "Title of the dashboard.")
public String getTitle() { public String getTitle() {
return title; return title;
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -146,7 +146,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.name = name; this.name = name;
} }
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor") @ApiModelProperty(position = 6, value = "Device Profile Name", example = "Temperature Sensor")
public String getType() { public String getType() {
return type; return type;
} }
@ -155,7 +155,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.type = type; this.type = type;
} }
@ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "Room 234 Sensor") @ApiModelProperty(position = 7, value = "Label that may be used in widgets", example = "Room 234 Sensor")
public String getLabel() { public String getLabel() {
return label; return label;
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -40,7 +40,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
private static final long serialVersionUID = 5582010124562018986L; private static final long serialVersionUID = 5582010124562018986L;
@ApiModelProperty(position = 7, required = true, value = "JSON object with the referenced Entity Id (Device or Asset).") @ApiModelProperty(position = 7, value = "JSON object with the referenced Entity Id (Device or Asset).")
private EntityId entityId; private EntityId entityId;
private TenantId tenantId; private TenantId tenantId;
private CustomerId customerId; private CustomerId customerId;
@ -52,7 +52,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
@Length(fieldName = "type") @Length(fieldName = "type")
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor") @ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor")
private String type; private String type;
@ApiModelProperty(position = 8, required = true, value = "Set of telemetry and attribute keys to expose via Entity View.") @ApiModelProperty(position = 8, value = "Set of telemetry and attribute keys to expose via Entity View.")
private TelemetryEntityView keys; private TelemetryEntityView keys;
@ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;") @ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;")
private long startTimeMs; private long startTimeMs;

4
common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java

@ -20,13 +20,17 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import javax.validation.constraints.NotNull;
@ApiModel @ApiModel
@Data @Data
public class SaveDeviceWithCredentialsRequest { public class SaveDeviceWithCredentialsRequest {
@ApiModelProperty(position = 1, value = "The JSON with device entity.", required = true) @ApiModelProperty(position = 1, value = "The JSON with device entity.", required = true)
@NotNull
private final Device device; private final Device device;
@ApiModelProperty(position = 2, value = "The JSON with credentials entity.", required = true) @ApiModelProperty(position = 2, value = "The JSON with credentials entity.", required = true)
@NotNull
private final DeviceCredentials credentials; private final DeviceCredentials credentials;
} }

18
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -34,14 +34,14 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
@Length(fieldName = "title") @Length(fieldName = "title")
@NoXss @NoXss
@ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A") @ApiModelProperty(position = 3, required = true, value = "Title of the tenant", example = "Company A")
private String title; private String title;
@NoXss @NoXss
@Length(fieldName = "region") @Length(fieldName = "region")
@ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America") @ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America")
private String region; private String region;
@ApiModelProperty(position = 6, required = true, value = "JSON object with Tenant Profile Id") @ApiModelProperty(position = 6, value = "JSON object with Tenant Profile Id")
private TenantProfileId tenantProfileId; private TenantProfileId tenantProfileId;
public Tenant() { public Tenant() {
@ -111,43 +111,43 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
return super.getCreatedTime(); return super.getCreatedTime();
} }
@ApiModelProperty(position = 7, required = true, value = "Country", example = "US") @ApiModelProperty(position = 7, value = "Country", example = "US")
@Override @Override
public String getCountry() { public String getCountry() {
return super.getCountry(); return super.getCountry();
} }
@ApiModelProperty(position = 8, required = true, value = "State", example = "NY") @ApiModelProperty(position = 8, value = "State", example = "NY")
@Override @Override
public String getState() { public String getState() {
return super.getState(); return super.getState();
} }
@ApiModelProperty(position = 9, required = true, value = "City", example = "New York") @ApiModelProperty(position = 9, value = "City", example = "New York")
@Override @Override
public String getCity() { public String getCity() {
return super.getCity(); return super.getCity();
} }
@ApiModelProperty(position = 10, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400") @ApiModelProperty(position = 10, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override @Override
public String getAddress() { public String getAddress() {
return super.getAddress(); return super.getAddress();
} }
@ApiModelProperty(position = 11, required = true, value = "Address Line 2", example = "") @ApiModelProperty(position = 11, value = "Address Line 2", example = "")
@Override @Override
public String getAddress2() { public String getAddress2() {
return super.getAddress2(); return super.getAddress2();
} }
@ApiModelProperty(position = 12, required = true, value = "Zip code", example = "10004") @ApiModelProperty(position = 12, value = "Zip code", example = "10004")
@Override @Override
public String getZip() { public String getZip() {
return super.getZip(); return super.getZip();
} }
@ApiModelProperty(position = 13, required = true, value = "Phone number", example = "+1(415)777-7777") @ApiModelProperty(position = 13, value = "Phone number", example = "+1(415)777-7777")
@Override @Override
public String getPhone() { public String getPhone() {
return super.getPhone(); return super.getPhone();

6
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -129,7 +129,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.authority = authority; this.authority = authority;
} }
@ApiModelProperty(position = 8, required = true, value = "First name of the user", example = "John") @ApiModelProperty(position = 8, required = false, value = "First name of the user", example = "John")
public String getFirstName() { public String getFirstName() {
return firstName; return firstName;
} }
@ -138,7 +138,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.firstName = firstName; this.firstName = firstName;
} }
@ApiModelProperty(position = 9, required = true, value = "Last name of the user", example = "Doe") @ApiModelProperty(position = 9, required = false, value = "Last name of the user", example = "Doe")
public String getLastName() { public String getLastName() {
return lastName; return lastName;
} }
@ -147,7 +147,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.lastName = lastName; this.lastName = lastName;
} }
@ApiModelProperty(position = 10, required = true, value = "Phone number of the user", example = "38012345123") @ApiModelProperty(position = 10, required = false, value = "Phone number of the user", example = "38012345123")
public String getPhone() { public String getPhone() {
return phone; return phone;
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -153,7 +153,6 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
} }
public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) { public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) {
if (cleared) { if (cleared) {
return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
} else { } else {

6
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -131,7 +131,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.name = name; this.name = name;
} }
@ApiModelProperty(position = 6, required = true, value = "Asset type", example = "Building") @ApiModelProperty(position = 6, value = "Asset type", example = "Building")
public String getType() { public String getType() {
return type; return type;
} }
@ -140,7 +140,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.type = type; this.type = type;
} }
@ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "NY Building") @ApiModelProperty(position = 7, value = "Label that may be used in widgets", example = "NY Building")
public String getLabel() { public String getLabel() {
return label; return label;
} }
@ -149,7 +149,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.label = label; this.label = label;
} }
@ApiModelProperty(position = 8, required = true, value = "JSON object with Asset Profile Id.") @ApiModelProperty(position = 8, value = "JSON object with Asset Profile Id.")
public AssetProfileId getAssetProfileId() { public AssetProfileId getAssetProfileId() {
return assetProfileId; return assetProfileId;
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java

@ -31,6 +31,7 @@ import java.util.UUID;
@ToString(callSuper = true) @ToString(callSuper = true)
public class EdgeEvent extends BaseData<EdgeEventId> { public class EdgeEvent extends BaseData<EdgeEventId> {
private long seqId;
private TenantId tenantId; private TenantId tenantId;
private EdgeId edgeId; private EdgeId edgeId;
private EdgeEventActionType action; private EdgeEventActionType action;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java

@ -42,6 +42,8 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
private String alarmOriginatorName; private String alarmOriginatorName;
private AlarmSeverity alarmSeverity; private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus; private AlarmStatus alarmStatus;
private boolean acknowledged;
private boolean cleared;
private CustomerId alarmCustomerId; private CustomerId alarmCustomerId;
@Override @Override

15
common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.msg; package org.thingsboard.server.common.msg;
import lombok.Getter;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@ -28,7 +29,7 @@ public enum MsgType {
* *
* See {@link PartitionChangeMsg} * See {@link PartitionChangeMsg}
*/ */
PARTITION_CHANGE_MSG, PARTITION_CHANGE_MSG(true),
APP_INIT_MSG, APP_INIT_MSG,
@ -108,7 +109,7 @@ public enum MsgType {
* Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement * Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement
*/ */
SESSION_TIMEOUT_MSG, SESSION_TIMEOUT_MSG(true),
STATS_PERSIST_TICK_MSG, STATS_PERSIST_TICK_MSG,
@ -130,4 +131,14 @@ public enum MsgType {
EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG, EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG,
EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG; EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG;
@Getter
private final boolean ignoreOnStart;
MsgType() {
this.ignoreOnStart = false;
}
MsgType(boolean ignoreOnStart) {
this.ignoreOnStart = ignoreOnStart;
}
} }

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

@ -174,10 +174,6 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Transactional @Transactional
@Override @Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) {
if (device.getId() == null) {
Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName());
device = deviceWithName == null ? device : deviceWithName.updateDevice(device);
}
Device savedDevice = this.saveDeviceWithoutCredentials(device, true); Device savedDevice = this.saveDeviceWithoutCredentials(device, true);
deviceCredentials.setDeviceId(savedDevice.getId()); deviceCredentials.setDeviceId(savedDevice.getId());
if (device.getId() == null) { if (device.getId() == null) {

4
dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java

@ -42,8 +42,8 @@ public class BaseEdgeEventService implements EdgeEventService {
} }
@Override @Override
public PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { public PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) {
return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, pageLink, withTsUpdate); return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, seqIdStart, seqIdEnd, pageLink);
} }
@Override @Override

4
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java

@ -43,10 +43,12 @@ public interface EdgeEventDao extends Dao<EdgeEvent> {
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param edgeId the edgeId * @param edgeId the edgeId
* @param seqIdStart the seq id start
* @param seqIdEnd the seq id end
* @param pageLink the pageLink * @param pageLink the pageLink
* @return the event list * @return the event list
*/ */
PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink);
/** /**
* Executes stored procedure to cleanup old edge events. * Executes stored procedure to cleanup old edge events.

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -535,6 +535,7 @@ public class ModelConstants {
*/ */
public static final String EDGE_EVENT_TABLE_NAME = "edge_event"; public static final String EDGE_EVENT_TABLE_NAME = "edge_event";
public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String EDGE_EVENT_SEQUENTIAL_ID_PROPERTY = "seq_id";
public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id"; public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id";
public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type"; public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type";
public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action"; public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action";

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java

@ -43,6 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PR
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_SEQUENTIAL_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY;
@ -57,6 +58,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@NoArgsConstructor @NoArgsConstructor
public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEntity<EdgeEvent> { public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEntity<EdgeEvent> {
@Column(name = EDGE_EVENT_SEQUENTIAL_ID_PROPERTY)
protected long seqId;
@Column(name = EDGE_EVENT_TENANT_ID_PROPERTY) @Column(name = EDGE_EVENT_TENANT_ID_PROPERTY)
private UUID tenantId; private UUID tenantId;
@ -120,6 +124,7 @@ public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEnt
edgeEvent.setAction(edgeEventAction); edgeEvent.setAction(edgeEventAction);
edgeEvent.setBody(entityBody); edgeEvent.setBody(entityBody);
edgeEvent.setUid(edgeEventUid); edgeEvent.setUid(edgeEventUid);
edgeEvent.setSeqId(seqId);
return edgeEvent; return edgeEvent;
} }

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

@ -20,7 +20,11 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
@ -31,6 +35,7 @@ import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
@ -45,7 +50,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TbResourceDaoService") @Service("TbResourceDaoService")
@Slf4j @Slf4j
@AllArgsConstructor @AllArgsConstructor
public class BaseResourceService implements ResourceService { public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
private final TbResourceDao resourceDao; private final TbResourceDao resourceDao;
@ -55,10 +60,12 @@ public class BaseResourceService implements ResourceService {
@Override @Override
public TbResource saveResource(TbResource resource) { public TbResource saveResource(TbResource resource) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId); resourceValidator.validate(resource, TbResourceInfo::getTenantId);
try { try {
return resourceDao.save(resource.getTenantId(), resource); TbResource saved = resourceDao.save(resource.getTenantId(), resource);
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
return saved;
} catch (Exception t) { } catch (Exception t) {
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) { if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) {
String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName"; String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName";
@ -86,7 +93,9 @@ public class BaseResourceService implements ResourceService {
public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
return resourceInfoDao.findById(tenantId, resourceId.getId());
return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId),
() -> resourceInfoDao.findById(tenantId, resourceId.getId()), true);
} }
@Override @Override
@ -100,6 +109,7 @@ public class BaseResourceService implements ResourceService {
public void deleteResource(TenantId tenantId, TbResourceId resourceId) { public void deleteResource(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId); log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
resourceValidator.validateDelete(tenantId, resourceId);
resourceDao.removeById(tenantId, resourceId.getId()); resourceDao.removeById(tenantId, resourceId.getId());
} }
@ -169,13 +179,11 @@ public class BaseResourceService implements ResourceService {
} }
}; };
protected Optional<ConstraintViolationException> extractConstraintViolationException(Exception t) { @TransactionalEventListener(classes = ResourceInfoEvictEvent.class)
if (t instanceof ConstraintViolationException) { @Override
return Optional.of((ConstraintViolationException) t); public void handleEvictEvent(ResourceInfoEvictEvent event) {
} else if (t.getCause() instanceof ConstraintViolationException) { if (event.getResourceId() != null) {
return Optional.of((ConstraintViolationException) (t.getCause())); cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId()));
} else {
return Optional.empty();
} }
} }
} }

4
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -22,6 +22,7 @@ import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.TenantEntityWithDataDao;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -82,6 +83,9 @@ public abstract class DataValidator<D extends BaseData<?>> {
return null; return null;
} }
public void validateDelete(TenantId tenantId, EntityId entityId) {
}
protected boolean isSameData(D existentData, D actualData) { protected boolean isSameData(D existentData, D actualData) {
return actualData.getId() != null && existentData.getId().equals(actualData.getId()); return actualData.getId() != null && existentData.getId().equals(actualData.getId());
} }

21
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -20,14 +20,21 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.TbResourceDao; import org.thingsboard.server.dao.resource.TbResourceDao;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE; import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE;
@ -37,6 +44,9 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Autowired @Autowired
private TbResourceDao resourceDao; private TbResourceDao resourceDao;
@Autowired
private WidgetTypeDao widgetTypeDao;
@Autowired @Autowired
private TenantService tenantService; private TenantService tenantService;
@ -77,4 +87,15 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
} }
} }
} }
@Override
public void validateDelete(TenantId tenantId, EntityId resourceId) {
List<WidgetTypeDetails> widgets = widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(),
resourceId.getId());
if (!widgets.isEmpty()) {
List<String> widgetNames = widgets.stream().map(BaseWidgetType::getName).collect(Collectors.toList());
throw new DataValidationException(String.format("Following widget types uses current resource: %s", widgetNames));
}
}
} }

21
dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java

@ -30,8 +30,10 @@ public interface EdgeEventRepository extends JpaRepository<EdgeEventEntity, UUID
@Query("SELECT e FROM EdgeEventEntity e WHERE " + @Query("SELECT e FROM EdgeEventEntity e WHERE " +
"e.tenantId = :tenantId " + "e.tenantId = :tenantId " +
"AND e.edgeId = :edgeId " + "AND e.edgeId = :edgeId " +
"AND (:startTime IS NULL OR e.createdTime > :startTime) " + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " +
"AND (:endTime IS NULL OR e.createdTime <= :endTime) " + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " +
"AND (:seqIdStart IS NULL OR e.seqId > :seqIdStart) " +
"AND (:seqIdEnd IS NULL OR e.seqId < :seqIdEnd) " +
"AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" "AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))"
) )
Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId,
@ -39,20 +41,7 @@ public interface EdgeEventRepository extends JpaRepository<EdgeEventEntity, UUID
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
@Param("startTime") Long startTime, @Param("startTime") Long startTime,
@Param("endTime") Long endTime, @Param("endTime") Long endTime,
@Param("seqIdStart") Long seqIdStart,
@Param("seqIdEnd") Long seqIdEnd,
Pageable pageable); Pageable pageable);
@Query("SELECT e FROM EdgeEventEntity e WHERE " +
"e.tenantId = :tenantId " +
"AND e.edgeId = :edgeId " +
"AND (:startTime IS NULL OR e.createdTime > :startTime) " +
"AND (:endTime IS NULL OR e.createdTime <= :endTime) " +
"AND e.edgeEventAction <> 'TIMESERIES_UPDATED' " +
"AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))"
)
Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(@Param("tenantId") UUID tenantId,
@Param("edgeId") UUID edgeId,
@Param("textSearch") String textSearch,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
Pageable pageable);
} }

43
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeEventId; import org.thingsboard.server.common.data.id.EdgeEventId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
@ -43,7 +44,9 @@ import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -118,7 +121,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
} }
}; };
queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, 1, statsFactory); queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, 1, statsFactory);
queue.init(logExecutor, v -> edgeEventInsertRepository.save(v), queue.init(logExecutor, edgeEventInsertRepository::save,
Comparator.comparing(EdgeEventEntity::getTs) Comparator.comparing(EdgeEventEntity::getTs)
); );
} }
@ -171,29 +174,23 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
@Override @Override
public PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { public PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) {
if (withTsUpdate) { List<SortOrder> sortOrders = new ArrayList<>();
return DaoUtil.toPageData( if (pageLink.getSortOrder() != null) {
edgeEventRepository sortOrders.add(pageLink.getSortOrder());
.findEdgeEventsByTenantIdAndEdgeId(
tenantId,
edgeId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getStartTime(),
pageLink.getEndTime(),
DaoUtil.toPageable(pageLink)));
} else {
return DaoUtil.toPageData(
edgeEventRepository
.findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(
tenantId,
edgeId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getStartTime(),
pageLink.getEndTime(),
DaoUtil.toPageable(pageLink)));
} }
sortOrders.add(new SortOrder("seqId"));
return DaoUtil.toPageData(
edgeEventRepository
.findEdgeEventsByTenantIdAndEdgeId(
tenantId,
edgeId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getStartTime(),
pageLink.getEndTime(),
seqIdStart,
seqIdEnd,
DaoUtil.toPageable(pageLink, sortOrders)));
} }
@Override @Override

3
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -69,7 +69,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
private static final Map<EntityType, String> entityTableMap = new HashMap<>(); private static final Map<EntityType, String> entityTableMap = new HashMap<>();
private static final Map<EntityType, String> entityNameColumns = new HashMap<>(); private static final Map<EntityType, String> entityNameColumns = new HashMap<>();
private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" + private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" +
" WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id) END as phone"; " WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id)" +
" WHEN entity.entity_type = 'USER' THEN (select phone from tb_user where id = entity_id) END as phone";
private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" + private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" +
" WHEN entity.entity_type = 'CUSTOMER' THEN (select zip from customer where id = entity_id) END as zip"; " WHEN entity.entity_type = 'CUSTOMER' THEN (select zip from customer where id = entity_id) END as zip";
private static final String SELECT_ADDRESS_2 = " CASE WHEN entity.entity_type = 'TENANT'" + private static final String SELECT_ADDRESS_2 = " CASE WHEN entity.entity_type = 'TENANT'" +

5
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -77,6 +77,11 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias)); return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias));
} }
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId));
}
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.WIDGET_TYPE; return EntityType.WIDGET_TYPE;

7
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java

@ -46,4 +46,11 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId, WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId,
@Param("bundleAlias") String bundleAlias, @Param("bundleAlias") String bundleAlias,
@Param("alias") String alias); @Param("alias") String alias);
@Query(value = "SELECT * FROM widget_type wt " +
"WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))",
nativeQuery = true)
List<WidgetTypeDetailsEntity> findWidgetTypesInfosByTenantIdAndResourceId(@Param("tenantId") UUID tenantId,
@Param("resourceId") UUID resourceId);
} }

8
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -83,4 +83,12 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails> {
*/ */
WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias); WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias);
/**
* Find widget types infos by tenantId and resourceId in descriptor.
*
* @param tenantId the tenantId
* @param tbResourceId the resourceId
* @return the list of widget types infos objects
*/
List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId);
} }

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

@ -21,6 +21,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
@ -37,6 +38,7 @@ import java.util.Optional;
public class WidgetTypeServiceImpl implements WidgetTypeService { public class WidgetTypeServiceImpl implements WidgetTypeService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias "; public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias ";
@Autowired @Autowired
private WidgetTypeDao widgetTypeDao; private WidgetTypeDao widgetTypeDao;
@ -96,6 +98,14 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
} }
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId) {
log.trace("Executing findWidgetTypesInfosByTenantIdAndResourceId, tenantId [{}], tbResourceId [{}]", tenantId, tbResourceId);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateId(tbResourceId, INCORRECT_RESOURCE_ID + tbResourceId);
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(), tbResourceId.getId());
}
@Override @Override
public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) { public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) {
log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias); log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias);

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

@ -720,6 +720,7 @@ CREATE TABLE IF NOT EXISTS edge (
); );
CREATE TABLE IF NOT EXISTS edge_event ( CREATE TABLE IF NOT EXISTS edge_event (
seq_id INT GENERATED ALWAYS AS IDENTITY,
id uuid NOT NULL, id uuid NOT NULL,
created_time bigint NOT NULL, created_time bigint NOT NULL,
edge_id uuid, edge_id uuid,
@ -731,6 +732,7 @@ CREATE TABLE IF NOT EXISTS edge_event (
tenant_id uuid, tenant_id uuid,
ts bigint NOT NULL ts bigint NOT NULL
) PARTITION BY RANGE(created_time); ) PARTITION BY RANGE(created_time);
ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
CREATE TABLE IF NOT EXISTS rpc ( CREATE TABLE IF NOT EXISTS rpc (
id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY, id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY,

26
dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java

@ -71,7 +71,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED); EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED);
edgeEventService.saveAsync(edgeEvent).get(); edgeEventService.saveAsync(edgeEvent).get();
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, new TimePageLink(1), false); PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, new TimePageLink(1));
Assert.assertFalse(edgeEvents.getData().isEmpty()); Assert.assertFalse(edgeEvents.getData().isEmpty());
EdgeEvent saved = edgeEvents.getData().get(0); EdgeEvent saved = edgeEvents.getData().get(0);
@ -113,7 +113,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Futures.allAsList(futures).get(); Futures.allAsList(futures).get();
TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime);
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink);
Assert.assertNotNull(edgeEvents.getData()); Assert.assertNotNull(edgeEvents.getData());
Assert.assertEquals(2, edgeEvents.getData().size()); Assert.assertEquals(2, edgeEvents.getData().size());
@ -122,7 +122,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Assert.assertTrue(edgeEvents.hasNext()); Assert.assertTrue(edgeEvents.hasNext());
Assert.assertNotNull(pageLink.nextPageLink()); Assert.assertNotNull(pageLink.nextPageLink());
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink.nextPageLink(), true); edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink.nextPageLink());
Assert.assertNotNull(edgeEvents.getData()); Assert.assertNotNull(edgeEvents.getData());
Assert.assertEquals(1, edgeEvents.getData().size()); Assert.assertEquals(1, edgeEvents.getData().size());
@ -132,26 +132,6 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
edgeEventService.cleanupEvents(1); edgeEventService.cleanupEvents(1);
} }
@Test
public void findEdgeEventsWithTsUpdateAndWithout() throws Exception {
EdgeId edgeId = new EdgeId(Uuids.timeBased());
DeviceId deviceId = new DeviceId(Uuids.timeBased());
TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime", SortOrder.Direction.ASC));
EdgeEvent edgeEventWithTsUpdate = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.TIMESERIES_UPDATED);
edgeEventService.saveAsync(edgeEventWithTsUpdate).get();
PageData<EdgeEvent> allEdgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true);
PageData<EdgeEvent> edgeEventsWithoutTsUpdate = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false);
Assert.assertNotNull(allEdgeEvents.getData());
Assert.assertNotNull(edgeEventsWithoutTsUpdate.getData());
Assert.assertEquals(1, allEdgeEvents.getData().size());
Assert.assertEquals(allEdgeEvents.getData().get(0).getUuidId(), edgeEventWithTsUpdate.getUuidId());
Assert.assertTrue(edgeEventsWithoutTsUpdate.getData().isEmpty());
}
private ListenableFuture<Void> saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { private ListenableFuture<Void> saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception {
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED);
edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); edgeEvent.setId(new EdgeEventId(Uuids.startOf(time)));

40
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -70,6 +71,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
@ -79,6 +81,7 @@ import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.sql.relation.RelationRepository; import org.thingsboard.server.dao.sql.relation.RelationRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -105,6 +108,8 @@ public class EntityServiceTest extends AbstractServiceTest {
@Autowired @Autowired
AssetService assetService; AssetService assetService;
@Autowired @Autowired
UserService userService;
@Autowired
AttributesService attributesService; AttributesService attributesService;
@Autowired @Autowired
DeviceService deviceService; DeviceService deviceService;
@ -227,6 +232,41 @@ public class EntityServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, count); Assert.assertEquals(0, count);
} }
@Test
public void testCountHierarchicalUserEntitiesByQuery() throws InterruptedException {
List<User> users = new ArrayList<>();
createTestUserRelations(tenantId, users);
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId);
filter.setDirection(EntitySearchDirection.FROM);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null);
List<EntityKey> entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "phone"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> entityDataByQuery = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
List<EntityData> data = entityDataByQuery.getData();
Assert.assertEquals(data.size(), 5);
data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone")));
}
private void createTestUserRelations(TenantId tenantId, List<User> users) {
for (int i = 0; i < ENTITY_COUNT; i++) {
User user = new User();
user.setTenantId(tenantId);
user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail(StringUtils.randomAlphabetic(10) + "@gmail.com");
user.setPhone(StringUtils.randomNumeric(10));
user = userService.saveUser(user);
users.add(user);
createRelation(tenantId, "Contains", tenantId, user.getId());
}
}
@Test @Test
public void testCountEdgeEntitiesByQuery() throws InterruptedException { public void testCountEdgeEntitiesByQuery() throws InterruptedException {
List<Edge> edges = new ArrayList<>(); List<Edge> edges = new ArrayList<>();

3
dao/src/test/resources/application-test.properties

@ -74,6 +74,9 @@ cache.specs.dashboardTitles.maxSize=10000
cache.specs.entityCount.timeToLiveInMinutes=1440 cache.specs.entityCount.timeToLiveInMinutes=1440
cache.specs.entityCount.maxSize=10000 cache.specs.entityCount.maxSize=10000
cache.specs.resourceInfo.timeToLiveInMinutes=1440
cache.specs.resourceInfo.maxSize=10000
redis.connection.host=localhost redis.connection.host=localhost
redis.connection.port=6379 redis.connection.port=6379
redis.connection.db=0 redis.connection.db=0

5
dao/src/test/resources/sql/system-test-psql.sql

@ -1,2 +1,5 @@
--PostgreSQL specific truncate to fit constraints --PostgreSQL specific truncate to fit constraints
TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm; TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm;
-- Decrease seq_id column to make sure to cover cases of new sequential cycle during the tests
ALTER SEQUENCE edge_event_seq_id_seq MAXVALUE 256;

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.rule.engine.aws.sns; package org.thingsboard.rule.engine.aws.sns;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.BasicAWSCredentials;
@ -71,6 +72,9 @@ public class TbSnsNode extends TbAbstractExternalNode {
this.snsClient = AmazonSNSClient.builder() this.snsClient = AmazonSNSClient.builder()
.withCredentials(credProvider) .withCredentials(credProvider)
.withRegion(this.config.getRegion()) .withRegion(this.config.getRegion())
.withClientConfiguration(new ClientConfiguration()
.withConnectionTimeout(10000)
.withRequestTimeout(5000))
.build(); .build();
} catch (Exception e) { } catch (Exception e) {
throw new TbNodeException(e); throw new TbNodeException(e);
@ -79,10 +83,10 @@ public class TbSnsNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
withCallback(publishMessageAsync(ctx, msg), var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m), m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t)); t -> tellFailure(ctx, processException(ctx, tbMsg, t), t));
ackIfNeeded(ctx, msg);
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.rule.engine.aws.sqs; package org.thingsboard.rule.engine.aws.sqs;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.BasicAWSCredentials;
@ -78,6 +79,9 @@ public class TbSqsNode extends TbAbstractExternalNode {
this.sqsClient = AmazonSQSClientBuilder.standard() this.sqsClient = AmazonSQSClientBuilder.standard()
.withCredentials(credProvider) .withCredentials(credProvider)
.withRegion(this.config.getRegion()) .withRegion(this.config.getRegion())
.withClientConfiguration(new ClientConfiguration()
.withConnectionTimeout(10000)
.withRequestTimeout(5000))
.build(); .build();
} catch (Exception e) { } catch (Exception e) {
throw new TbNodeException(e); throw new TbNodeException(e);
@ -86,10 +90,10 @@ public class TbSqsNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg), var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m), m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t)); t -> tellFailure(ctx, processException(ctx, tbMsg, t), t));
ackIfNeeded(ctx, msg);
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java

@ -52,9 +52,12 @@ public abstract class TbAbstractExternalNode implements TbNode {
} }
} }
protected void ackIfNeeded(TbContext ctx, TbMsg msg) { protected TbMsg ackIfNeeded(TbContext ctx, TbMsg msg) {
if (forceAck) { if (forceAck) {
ctx.ack(msg); ctx.ack(msg);
return msg.copyWithNewCtx();
} else {
return msg;
} }
} }

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java

@ -20,6 +20,7 @@ import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures; import com.google.api.core.ApiFutures;
import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.pubsub.v1.Publisher; import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
@ -36,6 +37,7 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.threeten.bp.Duration;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -75,8 +77,8 @@ public class TbPubSubNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
msg = ackIfNeeded(ctx, msg);
publishMessage(ctx, msg); publishMessage(ctx, msg);
ackIfNeeded(ctx, msg);
} }
@Override @Override
@ -134,8 +136,19 @@ public class TbPubSubNode extends TbAbstractExternalNode {
new ByteArrayInputStream(config.getServiceAccountKey().getBytes())); new ByteArrayInputStream(config.getServiceAccountKey().getBytes()));
CredentialsProvider credProvider = FixedCredentialsProvider.create(credentials); CredentialsProvider credProvider = FixedCredentialsProvider.create(credentials);
var retrySettings = RetrySettings.newBuilder()
.setTotalTimeout(Duration.ofSeconds(10))
.setInitialRetryDelay(Duration.ofMillis(50))
.setRetryDelayMultiplier(1.1)
.setMaxRetryDelay(Duration.ofSeconds(2))
.setInitialRpcTimeout(Duration.ofSeconds(2))
.setRpcTimeoutMultiplier(1)
.setMaxRpcTimeout(Duration.ofSeconds(10))
.build();
return Publisher.newBuilder(topicName) return Publisher.newBuilder(topicName)
.setCredentialsProvider(credProvider) .setCredentialsProvider(credProvider)
.setRetrySettings(retrySettings)
.build(); .build();
} }
} }

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java

@ -115,25 +115,25 @@ public class TbKafkaNode extends TbAbstractExternalNode {
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg); String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg);
String keyPattern = config.getKeyPattern(); String keyPattern = config.getKeyPattern();
var tbMsg = ackIfNeeded(ctx, msg);
try { try {
if (initError != null) { if (initError != null) {
ctx.tellFailure(msg, new RuntimeException("Failed to initialize Kafka rule node producer: " + initError.getMessage())); ctx.tellFailure(tbMsg, new RuntimeException("Failed to initialize Kafka rule node producer: " + initError.getMessage()));
} else { } else {
ctx.getExternalCallExecutor().executeAsync(() -> { ctx.getExternalCallExecutor().executeAsync(() -> {
publish( publish(
ctx, ctx,
msg, tbMsg,
topic, topic,
keyPattern == null || keyPattern.isEmpty() keyPattern == null || keyPattern.isEmpty()
? null ? null
: TbNodeUtils.processPattern(config.getKeyPattern(), msg) : TbNodeUtils.processPattern(config.getKeyPattern(), tbMsg)
); );
return null; return null;
}); });
} }
ackIfNeeded(ctx, msg);
} catch (Exception e) { } catch (Exception e) {
ctx.tellFailure(msg, e); ctx.tellFailure(tbMsg, e);
} }
} }

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java

@ -72,13 +72,13 @@ public class TbSendEmailNode extends TbAbstractExternalNode {
try { try {
validateType(msg.getType()); validateType(msg.getType());
TbEmail email = getEmail(msg); TbEmail email = getEmail(msg);
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(ctx.getMailExecutor().executeAsync(() -> { withCallback(ctx.getMailExecutor().executeAsync(() -> {
sendEmail(ctx, msg, email); sendEmail(ctx, tbMsg, email);
return null; return null;
}), }),
ok -> tellSuccess(ctx, msg), ok -> tellSuccess(ctx, tbMsg),
fail -> tellFailure(ctx, msg, fail)); fail -> tellFailure(ctx, tbMsg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception ex) { } catch (Exception ex) {
ctx.tellFailure(msg, ex); ctx.tellFailure(msg, ex);
} }

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -80,16 +80,16 @@ public class TbMqttNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg); String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg);
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) var tbMsg = ackIfNeeded(ctx, msg);
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(tbMsg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
.addListener(future -> { .addListener(future -> {
if (future.isSuccess()) { if (future.isSuccess()) {
tellSuccess(ctx, msg); tellSuccess(ctx, tbMsg);
} else { } else {
tellFailure(ctx, processException(ctx, msg, future.cause()), future.cause()); tellFailure(ctx, processException(ctx, tbMsg, future.cause()), future.cause());
} }
} }
); );
ackIfNeeded(ctx, msg);
} }
private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java

@ -71,16 +71,17 @@ public class TbNotificationNode extends TbAbstractExternalNode {
.originatorEntityId(ctx.getSelf().getRuleChainId()) .originatorEntityId(ctx.getSelf().getRuleChainId())
.build(); .build();
var tbMsg = ackIfNeeded(ctx, msg);
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() ->
ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> {
TbMsgMetaData metaData = msg.getMetaData().copy(); TbMsgMetaData metaData = tbMsg.getMetaData().copy();
metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats));
tellSuccess(ctx, TbMsg.transformMsg(msg, metaData)); tellSuccess(ctx, TbMsg.transformMsg(tbMsg, metaData));
})), })),
r -> { r -> {
}, },
e -> tellFailure(ctx, msg, e)); e -> tellFailure(ctx, tbMsg, e));
ackIfNeeded(ctx, msg);
} }
} }

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java

@ -61,12 +61,12 @@ public class TbSlackNode extends TbAbstractExternalNode {
} }
String message = TbNodeUtils.processPattern(config.getMessageTemplate(), msg); String message = TbNodeUtils.processPattern(config.getMessageTemplate(), msg);
var tbMsg = ackIfNeeded(ctx, msg);
DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> { DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> {
ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message); ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message);
}), }),
r -> tellSuccess(ctx, msg), r -> tellSuccess(ctx, tbMsg),
e -> tellFailure(ctx, msg, e)); e -> tellFailure(ctx, tbMsg, e));
ackIfNeeded(ctx, msg);
} }
} }

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -76,6 +76,10 @@ public class TbDeviceProfileNode implements TbNode {
this.ctx = ctx; this.ctx = ctx;
scheduleAlarmHarvesting(ctx, null); scheduleAlarmHarvesting(ctx, null);
ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate); ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate);
initAlarmRuleState(false);
}
private void initAlarmRuleState(boolean printNewlyAddedDeviceStates) {
if (config.isFetchAlarmRulesStateOnStart()) { if (config.isFetchAlarmRulesStateOnStart()) {
log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); log.info("[{}] Fetching alarm rule state", ctx.getSelfId());
int fetchCount = 0; int fetchCount = 0;
@ -86,7 +90,7 @@ public class TbDeviceProfileNode implements TbNode {
for (RuleNodeState rns : states.getData()) { for (RuleNodeState rns : states.getData()) {
fetchCount++; fetchCount++;
if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) {
getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns, printNewlyAddedDeviceStates);
} }
} }
} }
@ -130,7 +134,7 @@ public class TbDeviceProfileNode implements TbNode {
removeDeviceState(deviceId); removeDeviceState(deviceId);
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
} else { } else {
DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null); DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null, false);
if (deviceState != null) { if (deviceState != null) {
deviceState.process(ctx, msg); deviceState.process(ctx, msg);
} else { } else {
@ -148,6 +152,7 @@ public class TbDeviceProfileNode implements TbNode {
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
// Cleanup the cache for all entities that are no longer assigned to current server partitions // Cleanup the cache for all entities that are no longer assigned to current server partitions
deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey()));
initAlarmRuleState(true);
} }
@Override @Override
@ -156,13 +161,16 @@ public class TbDeviceProfileNode implements TbNode {
deviceStates.clear(); deviceStates.clear();
} }
protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns) { protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) {
DeviceState deviceState = deviceStates.get(deviceId); DeviceState deviceState = deviceStates.get(deviceId);
if (deviceState == null) { if (deviceState == null) {
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId);
if (deviceProfile != null) { if (deviceProfile != null) {
deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns); deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns);
deviceStates.put(deviceId, deviceState); deviceStates.put(deviceId, deviceState);
if (printNewlyAddedDeviceStates) {
log.info("[{}][{}] Device [{}] was added during PartitionChangeMsg", ctx.getTenantId(), ctx.getSelfId(), deviceId);
}
} }
} }
return deviceState; return deviceState;

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java

@ -84,10 +84,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg), var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m), m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t)); t -> tellFailure(ctx, processException(ctx, tbMsg, t), t));
ackIfNeeded(ctx, msg);
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -60,10 +60,10 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
httpClient.processMessage(ctx, msg, var tbMsg = ackIfNeeded(ctx, msg);
httpClient.processMessage(ctx, tbMsg,
m -> tellSuccess(ctx, m), m -> tellSuccess(ctx, m),
(m, t) -> tellFailure(ctx, m, t)); (m, t) -> tellFailure(ctx, m, t));
ackIfNeeded(ctx, msg);
} }
@Override @Override

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java

@ -60,16 +60,16 @@ public class TbSendSmsNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
var tbMsg = ackIfNeeded(ctx, msg);
try { try {
withCallback(ctx.getSmsExecutor().executeAsync(() -> { withCallback(ctx.getSmsExecutor().executeAsync(() -> {
sendSms(ctx, msg); sendSms(ctx, tbMsg);
return null; return null;
}), }),
ok -> tellSuccess(ctx, msg), ok -> tellSuccess(ctx, tbMsg),
fail -> tellFailure(ctx, msg, fail)); fail -> tellFailure(ctx, tbMsg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception ex) { } catch (Exception ex) {
ctx.tellFailure(msg, ex); ctx.tellFailure(tbMsg, ex);
} }
} }

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

1
ui-ngx/angular.json

@ -82,6 +82,7 @@
], ],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"src/form.scss",
"node_modules/jquery.terminal/css/jquery.terminal.min.css", "node_modules/jquery.terminal/css/jquery.terminal.min.css",
"node_modules/tooltipster/dist/css/tooltipster.bundle.min.css", "node_modules/tooltipster/dist/css/tooltipster.bundle.min.css",
"node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css", "node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css",

47
ui-ngx/src/app/app.component.ts

@ -30,6 +30,7 @@ import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { svgIcons } from '@shared/models/icon.models';
@Component({ @Component({
selector: 'tb-root', selector: 'tb-root',
@ -55,44 +56,14 @@ export class AppComponent implements OnInit {
} }
}); });
this.matIconRegistry.addSvgIconLiteral( for (const svgIcon of Object.keys(svgIcons)) {
'google-logo', this.matIconRegistry.addSvgIconLiteral(
this.domSanitizer.bypassSecurityTrustHtml( svgIcon,
'<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>' this.domSanitizer.bypassSecurityTrustHtml(
) svgIcons[svgIcon]
); )
);
this.matIconRegistry.addSvgIconLiteral( }
'github-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 32.7 32.7"><path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.2-.4-.7-2.1.2-4.3 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5s2.8.2 4.1.5c3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3v4.5c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill="#211c19"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'facebook-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 263 263"><path d="M263 131.5C263 58.9 204.1 0 131.5 0S0 58.9 0 131.5c0 65.6 48.1 120 110.9 129.9v-91.9H77.5v-38h33.4v-29c0-33 19.6-51.2 49.7-51.2 14.4 0 29.4 2.6 29.4 2.6v32.4h-16.5c-16.3 0-21.4 10.1-21.4 20.5v24.7h36.4l-5.8 38h-30.6v91.9c62.8-9.9 110.9-64.3 110.9-129.9z" fill="#1877f2"/><path d="M182.7 169.5l5.8-38H152v-24.7c0-10.4 5.1-20.5 21.4-20.5H190V53.9s-15-2.6-29.4-2.6c-30 0-49.7 18.2-49.7 51.2v29H77.5v38h33.4v91.9c6.7 1.1 13.6 1.6 20.5 1.6s13.9-.5 20.5-1.6v-91.9h30.8z" fill="#fff"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'apple-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'queues-list',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
)
);
this.storageService.testLocalStorage(); this.storageService.testLocalStorage();

7
ui-ngx/src/app/core/http/device.service.ts

@ -87,6 +87,13 @@ export class DeviceService {
return this.http.post<Device>('/api/device', device, defaultHttpOptionsFromConfig(config)); return this.http.post<Device>('/api/device', device, defaultHttpOptionsFromConfig(config));
} }
public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable<Device> {
return this.http.post<Device>('/api/device-with-credentials', {
device,
credentials
}, defaultHttpOptionsFromConfig(config));
}
public deleteDevice(deviceId: string, config?: RequestConfig) { public deleteDevice(deviceId: string, config?: RequestConfig) {
return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config)); return this.http.delete(`/api/device/${deviceId}`, defaultHttpOptionsFromConfig(config));
} }

2
ui-ngx/src/app/core/public-api.ts

@ -18,7 +18,7 @@ export * from './api/public-api';
export * from './http/public-api'; export * from './http/public-api';
export * from './local-storage/local-storage.service'; export * from './local-storage/local-storage.service';
export * from './services/public-api'; export * from './services/public-api';
export * from './ws/telemetry-websocket.service'; export * from './ws/public-api';
export * from './core.state'; export * from './core.state';
export * from './core.module'; export * from './core.module';
export * from './utils'; export * from './utils';

6
ui-ngx/src/app/core/services/dialog.service.ts

@ -103,7 +103,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
color color
} },
autoFocus: false
}).afterClosed(); }).afterClosed();
} }
@ -114,7 +115,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
icon icon
} },
autoFocus: false
}).afterClosed(); }).afterClosed();
} }

2142
ui-ngx/src/app/core/services/material-icons-codepoints.raw

File diff suppressed because it is too large

2
ui-ngx/src/app/core/services/menu.models.ts

@ -24,7 +24,6 @@ export interface MenuSection extends HasUUID{
type: MenuSectionType; type: MenuSectionType;
path: string; path: string;
icon: string; icon: string;
isMdiIcon?: boolean;
pages?: Array<MenuSection>; pages?: Array<MenuSection>;
opened?: boolean; opened?: boolean;
disabled?: boolean; disabled?: boolean;
@ -39,6 +38,5 @@ export interface HomeSection {
export interface HomeSectionPlace { export interface HomeSectionPlace {
name: string; name: string;
icon: string; icon: string;
isMdiIcon?: boolean;
path: string; path: string;
} }

52
ui-ngx/src/app/core/services/menu.service.ts

@ -111,8 +111,7 @@ export class MenuService {
name: 'tenant-profile.tenant-profiles', name: 'tenant-profile.tenant-profiles',
type: 'link', type: 'link',
path: '/tenantProfiles', path: '/tenantProfiles',
icon: 'mdi:alpha-t-box', icon: 'mdi:alpha-t-box'
isMdiIcon: true
}, },
{ {
id: 'resources', id: 'resources',
@ -133,8 +132,7 @@ export class MenuService {
name: 'resource.resources-library', name: 'resource.resources-library',
type: 'link', type: 'link',
path: '/resources/resources-library', path: '/resources/resources-library',
icon: 'mdi:rhombus-split', icon: 'mdi:rhombus-split'
isMdiIcon: true
} }
] ]
}, },
@ -144,7 +142,6 @@ export class MenuService {
type: 'link', type: 'link',
path: '/notification', path: '/notification',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [ pages: [
{ {
id: 'notification_inbox', id: 'notification_inbox',
@ -176,8 +173,7 @@ export class MenuService {
fullName: 'notification.notification-templates', fullName: 'notification.notification-templates',
type: 'link', type: 'link',
path: '/notification/templates', path: '/notification/templates',
icon: 'mdi:message-draw', icon: 'mdi:message-draw'
isMdiIcon: true
}, },
{ {
id: 'notification_rules', id: 'notification_rules',
@ -185,8 +181,7 @@ export class MenuService {
fullName: 'notification.notification-rules', fullName: 'notification.notification-rules',
type: 'link', type: 'link',
path: '/notification/rules', path: '/notification/rules',
icon: 'mdi:message-cog', icon: 'mdi:message-cog'
isMdiIcon: true
} }
] ]
}, },
@ -218,8 +213,7 @@ export class MenuService {
fullName: 'admin.notifications-settings', fullName: 'admin.notifications-settings',
type: 'link', type: 'link',
path: '/settings/notifications', path: '/settings/notifications',
icon: 'mdi:message-badge', icon: 'mdi:message-badge'
isMdiIcon: true
}, },
{ {
id: 'queues', id: 'queues',
@ -250,16 +244,14 @@ export class MenuService {
name: 'admin.2fa.2fa', name: 'admin.2fa.2fa',
type: 'link', type: 'link',
path: '/security-settings/2fa', path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication', icon: 'mdi:two-factor-authentication'
isMdiIcon: true
}, },
{ {
id: 'oauth2', id: 'oauth2',
name: 'admin.oauth2.oauth2', name: 'admin.oauth2.oauth2',
type: 'link', type: 'link',
path: '/security-settings/oauth2', path: '/security-settings/oauth2',
icon: 'mdi:shield-account', icon: 'mdi:shield-account'
isMdiIcon: true
} }
] ]
} }
@ -281,7 +273,6 @@ export class MenuService {
{ {
name: 'tenant-profile.tenant-profiles', name: 'tenant-profile.tenant-profiles',
icon: 'mdi:alpha-t-box', icon: 'mdi:alpha-t-box',
isMdiIcon: true,
path: '/tenantProfiles' path: '/tenantProfiles'
}, },
] ]
@ -327,7 +318,6 @@ export class MenuService {
{ {
name: 'admin.2fa.2fa', name: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication', icon: 'mdi:two-factor-authentication',
isMdiIcon: true,
path: '/settings/2fa' path: '/settings/2fa'
}, },
{ {
@ -361,8 +351,7 @@ export class MenuService {
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'mdi:alert-outline', icon: 'mdi:alert-outline'
isMdiIcon: true
}, },
{ {
id: 'dashboards', id: 'dashboards',
@ -413,16 +402,14 @@ export class MenuService {
name: 'device-profile.device-profiles', name: 'device-profile.device-profiles',
type: 'link', type: 'link',
path: '/profiles/deviceProfiles', path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box', icon: 'mdi:alpha-d-box'
isMdiIcon: true
}, },
{ {
id: 'asset_profiles', id: 'asset_profiles',
name: 'asset-profile.asset-profiles', name: 'asset-profile.asset-profiles',
type: 'link', type: 'link',
path: '/profiles/assetProfiles', path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box', icon: 'mdi:alpha-a-box'
isMdiIcon: true
} }
] ]
}, },
@ -513,8 +500,7 @@ export class MenuService {
name: 'resource.resources-library', name: 'resource.resources-library',
type: 'link', type: 'link',
path: '/resources/resources-library', path: '/resources/resources-library',
icon: 'mdi:rhombus-split', icon: 'mdi:rhombus-split'
isMdiIcon: true
} }
] ]
}, },
@ -524,7 +510,6 @@ export class MenuService {
type: 'link', type: 'link',
path: '/notification', path: '/notification',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [ pages: [
{ {
id: 'notification_inbox', id: 'notification_inbox',
@ -556,8 +541,7 @@ export class MenuService {
fullName: 'notification.notification-templates', fullName: 'notification.notification-templates',
type: 'link', type: 'link',
path: '/notification/templates', path: '/notification/templates',
icon: 'mdi:message-draw', icon: 'mdi:message-draw'
isMdiIcon: true
}, },
{ {
id: 'notification_rules', id: 'notification_rules',
@ -565,8 +549,7 @@ export class MenuService {
fullName: 'notification.notification-rules', fullName: 'notification.notification-rules',
type: 'link', type: 'link',
path: '/notification/rules', path: '/notification/rules',
icon: 'mdi:message-cog', icon: 'mdi:message-cog'
isMdiIcon: true
} }
] ]
}, },
@ -598,8 +581,7 @@ export class MenuService {
fullName: 'admin.notifications-settings', fullName: 'admin.notifications-settings',
type: 'link', type: 'link',
path: '/settings/notifications', path: '/settings/notifications',
icon: 'mdi:message-badge', icon: 'mdi:message-badge'
isMdiIcon: true
}, },
{ {
id: 'repository_settings', id: 'repository_settings',
@ -673,7 +655,6 @@ export class MenuService {
{ {
name: 'asset-profile.asset-profiles', name: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box', icon: 'mdi:alpha-a-box',
isMdiIcon: true,
path: '/profiles/assetProfiles' path: '/profiles/assetProfiles'
} }
] ]
@ -689,7 +670,6 @@ export class MenuService {
{ {
name: 'device-profile.device-profiles', name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box', icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/profiles/deviceProfiles' path: '/profiles/deviceProfiles'
}, },
{ {
@ -814,8 +794,7 @@ export class MenuService {
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'mdi:alert-outline', icon: 'mdi:alert-outline'
isMdiIcon: true
}, },
{ {
id: 'dashboards', id: 'dashboards',
@ -874,7 +853,6 @@ export class MenuService {
type: 'link', type: 'link',
path: '/notification', path: '/notification',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [ pages: [
{ {
id: 'notification_inbox', id: 'notification_inbox',

25
ui-ngx/src/app/core/services/resources.service.ts

@ -47,6 +47,7 @@ export interface ModulesWithFactories {
}) })
export class ResourcesService { export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {}; private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {}; private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {}; private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
@ -61,6 +62,30 @@ export class ResourcesService {
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache()); this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache());
} }
public loadJsonResource<T>(url: string, postProcess?: (data: T) => T): Observable<T> {
if (this.loadedJsonResources[url]) {
return this.loadedJsonResources[url].asObservable();
}
const subject = new ReplaySubject<any>();
this.loadedJsonResources[url] = subject;
this.http.get<T>(url).subscribe(
{
next: (o) => {
if (postProcess) {
o = postProcess(o);
}
this.loadedJsonResources[url].next(o);
this.loadedJsonResources[url].complete();
},
error: () => {
this.loadedJsonResources[url].error(new Error(`Unable to load ${url}`));
delete this.loadedJsonResources[url];
}
}
);
return subject.asObservable();
}
public loadResource(url: string): Observable<any> { public loadResource(url: string): Observable<any> {
if (this.loadedResources[url]) { if (this.loadedResources[url]) {
return this.loadedResources[url].asObservable(); return this.loadedResources[url].asObservable();

50
ui-ngx/src/app/core/services/script/node-script-test.service.ts

@ -23,8 +23,8 @@ import {
NodeScriptTestDialogComponent, NodeScriptTestDialogComponent,
NodeScriptTestDialogData NodeScriptTestDialogData
} from '@shared/components/dialog/node-script-test-dialog.component'; } from '@shared/components/dialog/node-script-test-dialog.component';
import { sortObjectKeys } from '@core/utils';
import { ScriptLanguage } from '@shared/models/rule-node.models'; import { ScriptLanguage } from '@shared/models/rule-node.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -37,56 +37,52 @@ export class NodeScriptTestService {
testNodeScript(script: string, scriptType: string, functionTitle: string, testNodeScript(script: string, scriptType: string, functionTitle: string,
functionName: string, argNames: string[], ruleNodeId: string, helpId?: string, functionName: string, argNames: string[], ruleNodeId: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> { scriptLang?: ScriptLanguage, debugEventBody?: DebugRuleNodeEventBody): Observable<string> {
if (ruleNodeId) { if (ruleNodeId && !debugEventBody) {
return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
switchMap((debugIn) => { switchMap((debugIn) => {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (debugIn) {
if (debugIn.data) {
try {
msg = JSON.parse(debugIn.data);
} catch (e) {}
}
if (debugIn.metadata) {
try {
metadata = JSON.parse(debugIn.metadata);
} catch (e) {}
}
msgType = debugIn.msgType;
}
return this.openTestScriptDialog(script, scriptType, functionTitle, return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, msg, metadata, msgType, helpId, scriptLang); functionName, argNames, debugIn, helpId, scriptLang);
}) })
); );
} else { } else {
return this.openTestScriptDialog(script, scriptType, functionTitle, return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, null, null, null, helpId, scriptLang); functionName, argNames, debugEventBody, helpId, scriptLang);
} }
} }
private openTestScriptDialog(script: string, scriptType: string, private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string,
functionTitle: string, functionName: string, argNames: string[], argNames: string[], eventBody: DebugRuleNodeEventBody, helpId?: string,
msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> { scriptLang?: ScriptLanguage): Observable<string> {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (eventBody && eventBody.data) {
try {
msg = JSON.parse(eventBody.data);
} catch (e) {}
}
if (!msg) { if (!msg) {
msg = { msg = {
temperature: 22.4, temperature: 22.4,
humidity: 78 humidity: 78
}; };
} }
if (eventBody && eventBody.metadata) {
try {
metadata = JSON.parse(eventBody.metadata);
} catch (e) {}
}
if (!metadata) { if (!metadata) {
metadata = { metadata = {
deviceName: 'Test Device', deviceName: 'Test Device',
deviceType: 'default', deviceType: 'default',
ts: new Date().getTime() + '' ts: new Date().getTime() + ''
}; };
} else {
metadata = sortObjectKeys(metadata);
} }
if (!msgType) { if (eventBody && eventBody.msgType) {
msgType = eventBody.msgType;
} else {
msgType = 'POST_TELEMETRY_REQUEST'; msgType = 'POST_TELEMETRY_REQUEST';
} }
return this.dialog.open<NodeScriptTestDialogComponent, NodeScriptTestDialogData, string>(NodeScriptTestDialogComponent, return this.dialog.open<NodeScriptTestDialogComponent, NodeScriptTestDialogData, string>(NodeScriptTestDialogComponent,

75
ui-ngx/src/app/core/services/utils.service.ts

@ -21,32 +21,31 @@ import { Inject, Injectable, NgZone } from '@angular/core';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models'; import { ExceptionData } from '@app/shared/models/error.models';
import { import {
base64toObj,
base64toString,
baseUrl, baseUrl,
createLabelFromDatasource, createLabelFromDatasource,
deepClone, deepClone,
deleteNullProperties, deleteNullProperties,
guid, hashCode, guid,
hashCode,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
isString, isString,
isUndefined, isUndefined,
objToBase64, objToBase64,
objToBase64URI, objToBase64URI
base64toString,
base64toObj
} from '@core/utils'; } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants'; import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models'; import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models'; import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults'; import jsonSchemaDefaults from 'json-schema-defaults';
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw'; import { Observable } from 'rxjs';
import { Observable, of, ReplaySubject } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators'; import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { import {
@ -56,6 +55,8 @@ import {
TelemetryType TelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DatePipe } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -86,13 +87,6 @@ const defaultAlarmFields: Array<string> = [
alarmFields.status.keyName alarmFields.status.keyName
]; ];
const commonMaterialIcons: Array<string> = ['more_horiz', 'more_vert', 'open_in_new',
'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
'share', 'add', 'edit', 'done', 'delete'];
// @dynamic // @dynamic
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -121,10 +115,9 @@ export class UtilsService {
defaultAlarmDataKeys: Array<DataKey> = []; defaultAlarmDataKeys: Array<DataKey> = [];
materialIcons: Array<string> = [];
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
private zone: NgZone, private zone: NgZone,
private datePipe: DatePipe,
private translate: TranslateService) { private translate: TranslateService) {
let frame: Element = null; let frame: Element = null;
try { try {
@ -180,6 +173,27 @@ export class UtilsService {
return deepClone(this.defaultAlarmDataKeys); return deepClone(this.defaultAlarmDataKeys);
} }
public defaultAlarmFieldContent(key: DataKey | {name: string}, value: any): string {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
if (alarmField.time) {
return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : '';
} else if (alarmField === alarmFields.severity) {
return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField === alarmFields.status) {
return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value;
} else if (alarmField === alarmFields.originatorType) {
return this.translate.instant(entityTypeTranslations.get(value).type);
} else if (alarmField.value === alarmFields.assignee.value) {
return '';
}
}
return value;
}
return '';
}
public generateObjectFromJsonSchema(schema: any): any { public generateObjectFromJsonSchema(schema: any): any {
const obj = jsonSchemaDefaults(schema); const obj = jsonSchemaDefaults(schema);
deleteNullProperties(obj); deleteNullProperties(obj);
@ -306,31 +320,6 @@ export class UtilsService {
return datasources; return datasources;
} }
public getMaterialIcons(): Observable<Array<string>> {
if (this.materialIcons.length) {
return of(this.materialIcons);
} else {
const materialIconsSubject = new ReplaySubject<Array<string>>();
this.zone.runOutsideAngular(() => {
const codepointsArray = materialIconsCodepoints
.split('\n')
.filter((codepoint) => codepoint && codepoint.length);
codepointsArray.forEach((codepoint) => {
const values = codepoint.split(' ');
if (values && values.length === 2) {
this.materialIcons.push(values[0]);
}
});
materialIconsSubject.next(this.materialIcons);
});
return materialIconsSubject.asObservable();
}
}
public getCommonMaterialIcons(): Array<string> {
return commonMaterialIcons;
}
public getMaterialColor(index: number) { public getMaterialColor(index: number) {
const colorIndex = index % materialColors.length; const colorIndex = index % materialColors.length;
return materialColors[colorIndex].value; return materialColors[colorIndex].value;
@ -411,7 +400,7 @@ export class UtilsService {
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string { public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
if (str && str.length) { if (str && str.length) {
let hue = hashCode(str) % 360; const hue = hashCode(str) % 360;
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`; return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`;
} }
} }

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

@ -20,7 +20,7 @@ import { finalize, share } from 'rxjs/operators';
import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType, baseDetailsPageByEntityType } from '@shared/models/entity-type.models'; import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { serverErrorCodesTranslations } from '@shared/models/constants'; import { serverErrorCodesTranslations } from '@shared/models/constants';
@ -126,15 +126,6 @@ export function isString(value: any): boolean {
return typeof value === 'string'; return typeof value === 'string';
} }
export function isEmpty(obj: any): boolean {
for (const key of Object.keys(obj)) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
export function isLiteralObject(value: any) { export function isLiteralObject(value: any) {
return (!!value) && (value.constructor === Object); return (!!value) && (value.constructor === Object);
} }
@ -320,9 +311,29 @@ export function extractType<T extends object>(target: any, keysOfProps: (keyof T
return _.pick(target, keysOfProps); return _.pick(target, keysOfProps);
} }
export function isEqual(a: any, b: any): boolean { export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b);
return _.isEqual(a, b);
} export const isEmpty = (a: any): boolean => _.isEmpty(a);
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
if (a === b) {
return true;
}
if (isDefinedAndNotNull(a) && isDefinedAndNotNull(b)) {
return isEqual(a, b);
} else {
return (isUndefinedOrNull(a) || !a) && (isUndefinedOrNull(b) || !b);
}
};
export const isArraysEqualIgnoreUndefined = (a: any[], b: any[]): boolean => {
const res = isEqualIgnoreUndefined(a, b);
if (!res) {
return (isUndefinedOrNull(a) || !a?.length) && (isUndefinedOrNull(b) || !b?.length);
} else {
return res;
}
};
export function mergeDeep<T>(target: T, ...sources: T[]): T { export function mergeDeep<T>(target: T, ...sources: T[]): T {
return _.merge(target, ...sources); return _.merge(target, ...sources);

19
ui-ngx/src/app/core/ws/public-api.ts

@ -0,0 +1,19 @@
///
/// 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.
///
export * from './notification-websocket.service';
export * from './telemetry-websocket.service';
export * from './websocket.service';

8
ui-ngx/src/app/modules/common/modules-map.ts

@ -179,6 +179,10 @@ import * as ProtobufContentComponent from '@shared/components/protobuf-content.c
import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component'; import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component';
import * as StringItemsListComponent from '@shared/components/string-items-list.component'; import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component'; import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
import * as TbIconComponent from '@shared/components/icon.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -478,6 +482,10 @@ class ModulesMap implements IModulesMap {
'@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent, '@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent,
'@shared/components/string-items-list.component': StringItemsListComponent, '@shared/components/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent, '@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@shared/components/icon.component': TbIconComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent,

6
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html

@ -26,10 +26,14 @@
#userAutocomplete="matAutocomplete" #userAutocomplete="matAutocomplete"
[displayWith]="displayUserFn" [displayWith]="displayUserFn"
(optionSelected)="selected($event)"> (optionSelected)="selected($event)">
<mat-option [fxHide]="!assigneeId" [value]="null"> <mat-option *ngIf="displayAssigneeNotSet" [value]="assigneeOptions.noAssignee">
<mat-icon class="unassigned-icon">account_circle</mat-icon> <mat-icon class="unassigned-icon">account_circle</mat-icon>
<span>{{ assigneeNotSetText | translate }}</span> <span>{{ assigneeNotSetText | translate }}</span>
</mat-option> </mat-option>
<mat-option *ngIf="displayAssignedToCurrentUser" [value]="assigneeOptions.currentUser">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span>{{ assignedToCurrentUserText | translate }}</span>
</mat-option>
<mat-option *ngFor="let user of filteredUsers | async" [value]="user" <mat-option *ngFor="let user of filteredUsers | async" [value]="user"
[fxHide]="assigneeId === user.id.id"> [fxHide]="assigneeId === user.id.id">
<span class="user-avatar" [innerHTML]="getUserInitials(user)" <span class="user-avatar" [innerHTML]="getUserInitials(user)"

19
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts

@ -44,6 +44,7 @@ import { AlarmService } from '@core/http/alarm.service';
import { OverlayRef } from '@angular/cdk/overlay'; import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AlarmAssigneeOption } from '@shared/models/alarm.models';
export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken<any>('AlarmAssigneePanelData'); export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken<any>('AlarmAssigneePanelData');
@ -59,15 +60,19 @@ export interface AlarmAssigneePanelData {
}) })
export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy { export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy {
assigneeOptions = AlarmAssigneeOption;
private dirty = false; private dirty = false;
alarmId: string; alarmId: string;
assigneeId?: string; assigneeId?: string;
assigneeOption?: AlarmAssigneeOption = null;
assigneeNotSetText = 'alarm.unassigned'; assigneeNotSetText = 'alarm.unassigned';
assignedToCurrentUserText = '';
reassigned: boolean = false; reassigned = false;
selectUserFormGroup: FormGroup; selectUserFormGroup: FormGroup;
@ -77,6 +82,14 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
searchText = ''; searchText = '';
get displayAssigneeNotSet(): boolean {
return !!this.assigneeId;
}
get displayAssignedToCurrentUser(): boolean {
return false;
}
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData, constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData,
@ -124,8 +137,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
selected(event: MatAutocompleteSelectedEvent): void { selected(event: MatAutocompleteSelectedEvent): void {
this.clear(); this.clear();
const user: User = event.option.value; if (event.option.value?.id) {
if (user) { const user: User = event.option.value;
this.assign(user); this.assign(user);
} else { } else {
this.unassign(); this.unassign();

23
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts

@ -36,11 +36,14 @@ import { emptyPageData } from '@shared/models/page/page-data';
import { OverlayRef } from '@angular/cdk/overlay'; import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AlarmAssigneeOption } from '@shared/models/alarm.models';
export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData'); export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData');
export interface AlarmAssigneeSelectPanelData { export interface AlarmAssigneeSelectPanelData {
assigneeId?: string; assigneeId?: string;
assigneeOption?: AlarmAssigneeOption;
userMode?: boolean;
} }
@Component({ @Component({
@ -50,11 +53,15 @@ export interface AlarmAssigneeSelectPanelData {
}) })
export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy { export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy {
assigneeOptions = AlarmAssigneeOption;
private dirty = false; private dirty = false;
assigneeId?: string; assigneeId?: string;
assigneeOption?: AlarmAssigneeOption;
assigneeNotSetText = 'alarm.assignee-not-set'; assigneeNotSetText = 'alarm.assignee-not-set';
assignedToCurrentUserText = this.data.userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user';
selectUserFormGroup: FormGroup; selectUserFormGroup: FormGroup;
@ -67,6 +74,15 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
userSelected = false; userSelected = false;
result?: UserEmailInfo; result?: UserEmailInfo;
optionResult?: AlarmAssigneeOption;
get displayAssigneeNotSet(): boolean {
return this.assigneeOption !== AlarmAssigneeOption.noAssignee;
}
get displayAssignedToCurrentUser(): boolean {
return this.assigneeOption !== AlarmAssigneeOption.currentUser;
}
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -77,6 +93,7 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
private fb: FormBuilder, private fb: FormBuilder,
private utilsService: UtilsService) { private utilsService: UtilsService) {
this.assigneeId = data.assigneeId; this.assigneeId = data.assigneeId;
this.assigneeOption = data.assigneeOption;
this.selectUserFormGroup = this.fb.group({ this.selectUserFormGroup = this.fb.group({
user: [null] user: [null]
}); });
@ -112,7 +129,11 @@ export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit
selected(event: MatAutocompleteSelectedEvent): void { selected(event: MatAutocompleteSelectedEvent): void {
this.clear(); this.clear();
this.userSelected = true; this.userSelected = true;
this.result = event.option.value; if (event.option.value?.id) {
this.result = event.option.value;
} else {
this.optionResult = event.option.value;
}
this.overlayRef.dispose(); this.overlayRef.dispose();
} }

12
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html

@ -16,16 +16,16 @@
--> -->
<ng-container [formGroup]="assigneeFormGroup"> <ng-container [formGroup]="assigneeFormGroup">
<mat-form-field fxFlex class="mat-block" style="margin-bottom: 25px" <mat-form-field [ngStyle]="inline ? {width: '100%'} : {marginBottom: '25px'}"
(click)="openAlarmAssigneeSelectPanel($event)" (click)="openAlarmAssigneeSelectPanel($event)"
subscriptSizing="dynamic"> subscriptSizing="dynamic" [appearance]="inline ? 'outline' : 'fill'">
<mat-label translate>alarm.assignee</mat-label> <mat-label *ngIf="!inline" translate>alarm.assignee</mat-label>
<input matInput readonly formControlName="assignee"> <input matInput readonly formControlName="assignee">
<span *ngIf="assignee" matPrefix class="user-avatar" <span *ngIf="assignee" matPrefix class="user-avatar" [ngClass]="{'inline': inline}"
[style.backgroundColor]="getAvatarBgColor()"> [style.backgroundColor]="getAvatarBgColor()">
{{ getUserInitials() }} {{ getUserInitials() }}
</span> </span>
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon">account_circle</mat-icon> <mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon" [ngClass]="{'inline': inline}">account_circle</mat-icon>
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon> <mat-icon *ngIf="!disabled" matSuffix class="drop-down-icon" [ngClass]="{'inline': inline}">arrow_drop_down</mat-icon>
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>

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

Loading…
Cancel
Save