Browse Source

Merge remote-tracking branch 'origin/master' into feature/entities-version

pull/11112/head
ViacheslavKlimov 2 years ago
parent
commit
f7daf371fc
  1. 2
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 3
      application/src/main/data/json/system/widget_bundles/cards.json
  3. 2
      application/src/main/data/json/system/widget_bundles/html_widgets.json
  4. 2
      application/src/main/data/json/system/widget_types/bar_chart.json
  5. 2
      application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
  6. 2
      application/src/main/data/json/system/widget_types/bars.json
  7. 2
      application/src/main/data/json/system/widget_types/bars_deprecated.json
  8. 2
      application/src/main/data/json/system/widget_types/command_button.json
  9. 2
      application/src/main/data/json/system/widget_types/compass.json
  10. 2
      application/src/main/data/json/system/widget_types/doughnut.json
  11. 2
      application/src/main/data/json/system/widget_types/doughnut_deprecated.json
  12. 2
      application/src/main/data/json/system/widget_types/horizontal_doughnut.json
  13. 2
      application/src/main/data/json/system/widget_types/line_chart.json
  14. 2
      application/src/main/data/json/system/widget_types/markdown_html_card.json
  15. 2
      application/src/main/data/json/system/widget_types/pie.json
  16. 2
      application/src/main/data/json/system/widget_types/point_chart.json
  17. 2
      application/src/main/data/json/system/widget_types/polar_area.json
  18. 2
      application/src/main/data/json/system/widget_types/power_button.json
  19. 2
      application/src/main/data/json/system/widget_types/radar.json
  20. 2
      application/src/main/data/json/system/widget_types/range_chart.json
  21. 2
      application/src/main/data/json/system/widget_types/single_switch.json
  22. 2
      application/src/main/data/json/system/widget_types/slider.json
  23. 2
      application/src/main/data/json/system/widget_types/time_series_chart.json
  24. 2
      application/src/main/data/json/system/widget_types/timeseries_bar_chart.json
  25. 2
      application/src/main/data/json/system/widget_types/timeseries_line_chart.json
  26. 2
      application/src/main/data/json/system/widget_types/toggle_button.json
  27. 23
      application/src/main/data/json/system/widget_types/unread_notifications.json
  28. 2
      application/src/main/data/json/system/widget_types/update_json_attribute.json
  29. 491
      application/src/main/data/json/tenant/dashboards/gateways.json
  30. 9
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  31. 25
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  32. 3
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  33. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
  34. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
  35. 1
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  36. 57
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  37. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
  38. 11
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  39. 4
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java
  40. 6
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java
  41. 2
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java
  42. 1
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
  43. 12
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
  44. 2
      application/src/main/resources/thingsboard.yml
  45. 31
      application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
  46. 9
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  47. 1
      application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java
  48. 7
      application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java
  49. 8
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  50. 82
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  51. 23
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java
  52. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java
  53. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
  54. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java
  55. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  56. 5
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  57. 516
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  58. 302
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  59. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  60. 15
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  61. 6
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  62. 5
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java
  63. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  64. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  65. 17
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java
  66. 2
      pom.xml
  67. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java
  68. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  69. 24
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  70. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  71. 19
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java
  72. 2
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
  73. 10
      ui-ngx/src/app/core/utils.ts
  74. 6
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html
  75. 20
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.scss
  76. 2
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts
  77. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  78. 128
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html
  79. 177
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts
  80. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts
  81. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts
  82. 77
      ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html
  83. 25
      ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.scss
  84. 140
      ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts
  85. 54
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html
  86. 65
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss
  87. 284
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts
  88. 59
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts
  89. 4
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
  90. 4
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  91. 6
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts
  92. 263
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts
  93. 26
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html
  94. 252
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  95. 40
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  96. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html
  97. 130
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  98. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html
  99. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss
  100. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts

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

File diff suppressed because one or more lines are too long

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

@ -23,6 +23,7 @@
"cards.html_card", "cards.html_card",
"cards.html_value_card", "cards.html_value_card",
"cards.markdown_card", "cards.markdown_card",
"cards.simple_card" "cards.simple_card",
"unread_notifications"
] ]
} }

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -3,7 +3,7 @@
"name": "Bars", "name": "Bars",
"deprecated": true, "deprecated": true,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA8FBMVEUhlvNMr1Bqamp5eXl7e3t8fHx9fX1+fn5/f3+AgICCgoKDg4OEhISGhoaHh4eKioqMjIyNjY2Ojo6QkJCRkZGSkpKWlpaXl5ebm5udnZ2enp6goKChoaGkpKSnp6epqamsrKyurq6xsbGzs7O1tbW2tra3t7e4uLi7u7u9vb3BwcHCwsLDw8PGxsbKysrNzc3Ozs7R0dHS0tLT09PZ2dna2trc3Nzd3d3e3t7g4ODh4eHj4+Pk5OTm5ubn5+fo6Ojp6enu7u7w8PDz8/P0Qzb09PT29vb39/f5+fn6+vr7+/v8/Pz9/f3+/v7/wQf///+dc+aLAAAAAWJLR0RPbmZBSQAAAcFJREFUeNrt3ds2AgEYhuHsaSOZbAvZi0r2YYjCJOW7/7txZhkcDNbM6h/vdwfPmlX/ybtmEorJErGCeJLadz3rkKPpZamaLTp925DHdFvSpKelU9uQ/cLKhtcdk7YqtiHruevtojch7ZZtQ0o1dcdfRqXNqm1IbVU3OaVamm/YhvQW5zIXOknnC5JUt7qEpE5fUv/5HVePy2UHAgQIECBAgAABAgQIECBxgrwGHBAgQIAAAQIECBAgQIAAAQIECJC/QRIBN0iQ+66voDMLuRp2fQWdVUhvNun6CjqrkJ0Dx/UVdEYhzXzfcX0FnVFIrlSZ2mx/LOiMQuqHh6k972NBZ/fv13G/KeiCQkIu4358EL8UdBafSGwuOxAgQIAAAQIECJDYQB4CDggQIECAAAECBAgQIECAAAECBAgQIECA/BrSufn0DjqjkEZmLXkWaUEXEmThXMeFSAu68H4j5b1IC7rQILfZTqQFXViQ1nRTL1EWdCFBnmYuJUVZ0IUEWR1xHKcXZUEXFDLwBR2XHQgQIECAAAEC5H9ChgIOCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgxiBmv+L6Bl9pkxYph15gAAAAAElFTkSuQmCC", "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA8FBMVEUhlvNMr1Bqamp5eXl7e3t8fHx9fX1+fn5/f3+AgICCgoKDg4OEhISGhoaHh4eKioqMjIyNjY2Ojo6QkJCRkZGSkpKWlpaXl5ebm5udnZ2enp6goKChoaGkpKSnp6epqamsrKyurq6xsbGzs7O1tbW2tra3t7e4uLi7u7u9vb3BwcHCwsLDw8PGxsbKysrNzc3Ozs7R0dHS0tLT09PZ2dna2trc3Nzd3d3e3t7g4ODh4eHj4+Pk5OTm5ubn5+fo6Ojp6enu7u7w8PDz8/P0Qzb09PT29vb39/f5+fn6+vr7+/v8/Pz9/f3+/v7/wQf///+dc+aLAAAAAWJLR0RPbmZBSQAAAcFJREFUeNrt3ds2AgEYhuHsaSOZbAvZi0r2YYjCJOW7/7txZhkcDNbM6h/vdwfPmlX/ybtmEorJErGCeJLadz3rkKPpZamaLTp925DHdFvSpKelU9uQ/cLKhtcdk7YqtiHruevtojch7ZZtQ0o1dcdfRqXNqm1IbVU3OaVamm/YhvQW5zIXOknnC5JUt7qEpE5fUv/5HVePy2UHAgQIECBAgAABAgQIECBxgrwGHBAgQIAAAQIECBAgQIAAAQIECJC/QRIBN0iQ+66voDMLuRp2fQWdVUhvNun6CjqrkJ0Dx/UVdEYhzXzfcX0FnVFIrlSZ2mx/LOiMQuqHh6k972NBZ/fv13G/KeiCQkIu4358EL8UdBafSGwuOxAgQIAAAQIECJDYQB4CDggQIECAAAECBAgQIECAAAECBAgQIECA/BrSufn0DjqjkEZmLXkWaUEXEmThXMeFSAu68H4j5b1IC7rQILfZTqQFXViQ1nRTL1EWdCFBnmYuJUVZ0IUEWR1xHKcXZUEXFDLwBR2XHQgQIECAAAEC5H9ChgIOCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgxiBmv+L6Bl9pkxYph15gAAAAAElFTkSuQmCC",
"description": "Displays latest values of the attributes or time-series data for multiple entities as separate bars.", "description": "Displays latest values of the attributes or time series data for multiple entities as separate bars.",
"descriptor": { "descriptor": {
"type": "latest", "type": "latest",
"sizeX": 7, "sizeX": 7,

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -3,7 +3,7 @@
"name": "Doughnut", "name": "Doughnut",
"deprecated": false, "deprecated": false,
"image": "data:image/png;base64,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", "image": "data:image/png;base64,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",
"description": "Displays the latest values of the attributes or time-series data in a doughnut chart. Supports numeric values only.", "description": "Displays the latest values of the attributes or time series data in a doughnut chart. Supports numeric values only.",
"descriptor": { "descriptor": {
"type": "latest", "type": "latest",
"sizeX": 4, "sizeX": 4,

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

File diff suppressed because one or more lines are too long

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

@ -3,7 +3,7 @@
"name": "Horizontal doughnut", "name": "Horizontal doughnut",
"deprecated": false, "deprecated": false,
"image": "data:image/png;base64,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", "image": "data:image/png;base64,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",
"description": "Displays the latest values of the attributes or time-series data in a doughnut chart using horizontal layout. Supports numeric values only.", "description": "Displays the latest values of the attributes or time series data in a doughnut chart using horizontal layout. Supports numeric values only.",
"descriptor": { "descriptor": {
"type": "latest", "type": "latest",
"sizeX": 4, "sizeX": 4,

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -3,7 +3,7 @@
"name": "Pie", "name": "Pie",
"deprecated": false, "deprecated": false,
"image": "tb-image:cGllX2NoYXJ0LnN2Zw==:IlBpZSIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAxIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDUzOF8zNTMwNikiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjIuMzg4MDYiIGZpbGw9IndoaXRlIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPHBhdGggZD0iTTE0OC45NzUgMTI5LjQ5N0MxNTYuNzc0IDEyMS42OTggMTYyLjYyIDExMi4xNjYgMTY2LjAzNiAxMDEuNjc4TDk5LjQ3NzUgODBMMTQ4Ljk3NSAxMjkuNDk3WiIgZmlsbD0iI0ZGREUzMCIgc3Ryb2tlPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOTkuNDc3NSAxMEM4Ny45NjYgMTAgNzYuNjMyMSAxMi44MzkgNjYuNDc5OCAxOC4yNjU1QzU2LjMyNzUgMjMuNjkyIDQ3LjY3MDIgMzEuNTM4NiA0MS4yNzQ3IDQxLjExMDFDMzQuODc5MiA1MC42ODE2IDMwLjk0MjkgNjEuNjgyNyAyOS44MTQ2IDczLjEzODhDMjguNjg2MyA4NC41OTQ5IDMwLjQwMDcgOTYuMTUyNSAzNC44MDYgMTA2Ljc4OEMzOS4yMTEzIDExNy40MjMgNDYuMTcxNCAxMjYuODA4IDU1LjA3IDEzNC4xMTFDNjMuOTY4NiAxNDEuNDE0IDc0LjUzMDggMTQ2LjQwOSA4NS44MjEyIDE0OC42NTVDOTcuMTExNiAxNTAuOTAxIDEwOC43ODIgMTUwLjMyNyAxMTkuNzk3IDE0Ni45ODZDMTMwLjgxMyAxNDMuNjQ0IDE0MC44MzUgMTM3LjYzNyAxNDguOTc1IDEyOS40OTdMOTkuNDc3NSA4MFYxMFoiIGZpbGw9IiMwODg3MkIiIHN0cm9rZT0id2hpdGUiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTM4XzM1MzA2KSI+CjxwYXRoIGQ9Ik0xNjcuNTYyIDEwMi4wNTdDMTcxLjAxNCA5MS4yNTc1IDE3MS44ODIgNzkuNzg5OCAxNzAuMDk2IDY4LjU4ODVDMTY4LjMxMSA1Ny4zODcxIDE2My45MjIgNDYuNzY5MiAxNTcuMjg4IDM3LjYwMDJDMTUwLjY1MyAyOC40MzEyIDE0MS45NjEgMjAuOTcwNiAxMzEuOTIgMTUuODI2NkMxMjEuODc5IDEwLjY4MjYgMTEwLjc3MiA4LjAwMDc4IDk5LjUwNjQgNy45OTk3Nkw5OS41IDc5Ljk5OThMMTY3LjU2MiAxMDIuMDU3WiIgZmlsbD0iI0ZGNEQ1QSIvPgo8cGF0aCBkPSJNMTY3LjU2MiAxMDIuMDU3QzE3MS4wMTQgOTEuMjU3NSAxNzEuODgyIDc5Ljc4OTggMTcwLjA5NiA2OC41ODg1QzE2OC4zMTEgNTcuMzg3MSAxNjMuOTIyIDQ2Ljc2OTIgMTU3LjI4OCAzNy42MDAyQzE1MC42NTMgMjguNDMxMiAxNDEuOTYxIDIwLjk3MDYgMTMxLjkyIDE1LjgyNjZDMTIxLjg3OSAxMC42ODI2IDExMC43NzIgOC4wMDA3OCA5OS41MDY0IDcuOTk5NzZMOTkuNSA3OS45OTk4TDE2Ny41NjIgMTAyLjA1N1oiIHN0cm9rZT0id2hpdGUiLz4KPC9nPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDUzOF8zNTMwNiIgeD0iLTcuMTY0MTgiIHk9Ii00Ljc3NjEyIiB3aWR0aD0iMjE0LjMyOCIgaGVpZ2h0PSIxNzQuMzI4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIuMzg4MDYiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMy41ODIwOSIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDYgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzQ1MzhfMzUzMDYiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTM4XzM1MzA2IiB4PSI4NyIgeT0iLTQuNTAwMjQiIHdpZHRoPSI5Ni41IiBoZWlnaHQ9IjExOS4xODciIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjYiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjI1IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDUzOF8zNTMwNiIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=", "image": "tb-image:cGllX2NoYXJ0LnN2Zw==:IlBpZSIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAxIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDUzOF8zNTMwNikiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjIuMzg4MDYiIGZpbGw9IndoaXRlIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPHBhdGggZD0iTTE0OC45NzUgMTI5LjQ5N0MxNTYuNzc0IDEyMS42OTggMTYyLjYyIDExMi4xNjYgMTY2LjAzNiAxMDEuNjc4TDk5LjQ3NzUgODBMMTQ4Ljk3NSAxMjkuNDk3WiIgZmlsbD0iI0ZGREUzMCIgc3Ryb2tlPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOTkuNDc3NSAxMEM4Ny45NjYgMTAgNzYuNjMyMSAxMi44MzkgNjYuNDc5OCAxOC4yNjU1QzU2LjMyNzUgMjMuNjkyIDQ3LjY3MDIgMzEuNTM4NiA0MS4yNzQ3IDQxLjExMDFDMzQuODc5MiA1MC42ODE2IDMwLjk0MjkgNjEuNjgyNyAyOS44MTQ2IDczLjEzODhDMjguNjg2MyA4NC41OTQ5IDMwLjQwMDcgOTYuMTUyNSAzNC44MDYgMTA2Ljc4OEMzOS4yMTEzIDExNy40MjMgNDYuMTcxNCAxMjYuODA4IDU1LjA3IDEzNC4xMTFDNjMuOTY4NiAxNDEuNDE0IDc0LjUzMDggMTQ2LjQwOSA4NS44MjEyIDE0OC42NTVDOTcuMTExNiAxNTAuOTAxIDEwOC43ODIgMTUwLjMyNyAxMTkuNzk3IDE0Ni45ODZDMTMwLjgxMyAxNDMuNjQ0IDE0MC44MzUgMTM3LjYzNyAxNDguOTc1IDEyOS40OTdMOTkuNDc3NSA4MFYxMFoiIGZpbGw9IiMwODg3MkIiIHN0cm9rZT0id2hpdGUiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTM4XzM1MzA2KSI+CjxwYXRoIGQ9Ik0xNjcuNTYyIDEwMi4wNTdDMTcxLjAxNCA5MS4yNTc1IDE3MS44ODIgNzkuNzg5OCAxNzAuMDk2IDY4LjU4ODVDMTY4LjMxMSA1Ny4zODcxIDE2My45MjIgNDYuNzY5MiAxNTcuMjg4IDM3LjYwMDJDMTUwLjY1MyAyOC40MzEyIDE0MS45NjEgMjAuOTcwNiAxMzEuOTIgMTUuODI2NkMxMjEuODc5IDEwLjY4MjYgMTEwLjc3MiA4LjAwMDc4IDk5LjUwNjQgNy45OTk3Nkw5OS41IDc5Ljk5OThMMTY3LjU2MiAxMDIuMDU3WiIgZmlsbD0iI0ZGNEQ1QSIvPgo8cGF0aCBkPSJNMTY3LjU2MiAxMDIuMDU3QzE3MS4wMTQgOTEuMjU3NSAxNzEuODgyIDc5Ljc4OTggMTcwLjA5NiA2OC41ODg1QzE2OC4zMTEgNTcuMzg3MSAxNjMuOTIyIDQ2Ljc2OTIgMTU3LjI4OCAzNy42MDAyQzE1MC42NTMgMjguNDMxMiAxNDEuOTYxIDIwLjk3MDYgMTMxLjkyIDE1LjgyNjZDMTIxLjg3OSAxMC42ODI2IDExMC43NzIgOC4wMDA3OCA5OS41MDY0IDcuOTk5NzZMOTkuNSA3OS45OTk4TDE2Ny41NjIgMTAyLjA1N1oiIHN0cm9rZT0id2hpdGUiLz4KPC9nPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDUzOF8zNTMwNiIgeD0iLTcuMTY0MTgiIHk9Ii00Ljc3NjEyIiB3aWR0aD0iMjE0LjMyOCIgaGVpZ2h0PSIxNzQuMzI4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIuMzg4MDYiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMy41ODIwOSIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDYgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzQ1MzhfMzUzMDYiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTM4XzM1MzA2IiB4PSI4NyIgeT0iLTQuNTAwMjQiIHdpZHRoPSI5Ni41IiBoZWlnaHQ9IjExOS4xODciIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjYiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjI1IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDUzOF8zNTMwNiIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=",
"description": "Displays the latest values of the attributes or time-series data in a pie chart. Supports numeric values only.", "description": "Displays the latest values of the attributes or time series data in a pie chart. Supports numeric values only.",
"descriptor": { "descriptor": {
"type": "latest", "type": "latest",
"sizeX": 5, "sizeX": 5,

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -3,7 +3,7 @@
"name": "Timeseries Bar Chart", "name": "Timeseries Bar Chart",
"deprecated": true, "deprecated": true,
"image": "tb-image:dGltZXNlcmllc19iYXJfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlRpbWVzZXJpZXMgQmFyIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=", "image": "tb-image:dGltZXNlcmllc19iYXJfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlRpbWVzZXJpZXMgQmFyIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=",
"description": "Displays changes to time-series data over time—for example, daily water consumption for the last month.", "description": "Displays changes to time series data over time—for example, daily water consumption for the last month.",
"descriptor": { "descriptor": {
"type": "timeseries", "type": "timeseries",
"sizeX": 8, "sizeX": 8,

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/unread_notifications.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

491
application/src/main/data/json/tenant/dashboards/gateways.json

@ -880,7 +880,7 @@
"isConnectorLog": true, "isConnectorLog": true,
"connectorLogState": "connector_logs" "connectorLogState": "connector_logs"
}, },
"title": "${entityName} Logs", "title": "${connectorName} logs",
"showTitleIcon": false, "showTitleIcon": false,
"dropShadow": true, "dropShadow": true,
"enableFullscreen": true, "enableFullscreen": true,
@ -1085,13 +1085,14 @@
"showLegend": false, "showLegend": false,
"actions": {}, "actions": {},
"datasources": [], "datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false, "showTitleIcon": false,
"widgetCss": "", "widgetCss": "",
"pageSize": 1024, "pageSize": 1024,
"noDataDisplayMessage": "" "noDataDisplayMessage": "",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
@ -1494,15 +1495,16 @@
"showLegend": false, "showLegend": false,
"actions": {}, "actions": {},
"datasources": [], "datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false, "showTitleIcon": false,
"titleTooltip": "", "titleTooltip": "",
"widgetCss": "", "widgetCss": "",
"pageSize": 1024, "pageSize": 1024,
"noDataDisplayMessage": "", "noDataDisplayMessage": "",
"borderRadius": "4px" "borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
@ -1557,9 +1559,6 @@
}, },
"title": "Service RPC", "title": "Service RPC",
"datasources": [], "datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false, "showTitleIcon": false,
"titleTooltip": "", "titleTooltip": "",
"dropShadow": true, "dropShadow": true,
@ -1574,7 +1573,11 @@
"noDataDisplayMessage": "", "noDataDisplayMessage": "",
"enableDataExport": false, "enableDataExport": false,
"showLegend": false, "showLegend": false,
"borderRadius": "4px" "borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
@ -1596,9 +1599,6 @@
}, },
"title": "Service RPC", "title": "Service RPC",
"datasources": [], "datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false, "showTitleIcon": false,
"titleTooltip": "", "titleTooltip": "",
"dropShadow": true, "dropShadow": true,
@ -1613,7 +1613,11 @@
"noDataDisplayMessage": "", "noDataDisplayMessage": "",
"enableDataExport": false, "enableDataExport": false,
"showLegend": false, "showLegend": false,
"borderRadius": "4px" "borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
@ -1802,15 +1806,16 @@
"showLegend": false, "showLegend": false,
"actions": {}, "actions": {},
"datasources": [], "datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false, "showTitleIcon": false,
"titleTooltip": "", "titleTooltip": "",
"widgetCss": "", "widgetCss": "",
"pageSize": 1024, "pageSize": 1024,
"noDataDisplayMessage": "", "noDataDisplayMessage": "",
"borderRadius": "4px" "borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
}, },
"row": 0, "row": 0,
"col": 0, "col": 0,
@ -2102,6 +2107,448 @@
"ACTIVE" "ACTIVE"
] ]
} }
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "bd9176e1-9e04-3e9b-d5a5-07b72bb9ae90",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "mqttCount",
"color": "#4caf50",
"settings": {},
"_hash": 0.9590451878027946,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entity",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "44038462-1bae-e075-7b31-283341cb2295",
"dataKeys": [
{
"name": "count",
"type": "entityField",
"label": "modbusCount",
"color": "#4caf50",
"settings": {},
"_hash": 0.9300660062254784,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "884e9c34-7534-a483-99be-81b56cd91185",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "grcpCount",
"color": "#f44336",
"settings": {},
"_hash": 0.16110429492126088,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "e91ca0e9-1653-4fbc-5f3d-3da021b1b415",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "opcuaCount",
"color": "#ffc107",
"settings": {},
"_hash": 0.13973521146913304,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "3f74cbaa-6353-5e88-a7e8-708fc0e18039",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "bleCount",
"color": "#607d8b",
"settings": {},
"_hash": 0.7205723252294653,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "c08eee84-64ee-73c4-8d96-c0df813a92cd",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "requestCount",
"color": "#9c27b0",
"settings": {},
"_hash": 0.6993292961463216,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "2f0af7f5-22ea-c0d5-3aef-7f2bb1b534ec",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "canCount",
"color": "#8bc34a",
"settings": {},
"_hash": 0.4850065031079176,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "92a7d208-c143-ea20-5162-1da584532830",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "bacnetCount",
"color": "#3f51b5",
"settings": {},
"_hash": 0.36987531665233075,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "498f090c-b1e5-df74-35d1-3ecf89d33f1c",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "odbcCount",
"color": "#e91e63",
"settings": {},
"_hash": 0.8279345016611896,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "9175179d-a8db-848b-0762-e78da150e768",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "restCount",
"color": "#ffeb3b",
"settings": {},
"_hash": 0.07245826389852739,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "9175179d-a8db-848b-0762-e78da150e768",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "restCount",
"color": "#03a9f4",
"settings": {},
"_hash": 0.4321492578560704,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "1b70460b-428b-2aed-f23a-65927d3e67bb",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "snmpCount",
"color": "#ff9800",
"settings": {},
"_hash": 0.7395574625078822,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "ae357478-b4c2-eee8-dde6-a6942fe6202f",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "ftpCount",
"color": "#673ab7",
"settings": {},
"_hash": 0.9999791065203374,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "6eef4979-369f-c2cc-4894-96a84b6a668a",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "socketCount",
"color": "#cddc39",
"settings": {},
"_hash": 0.47563823619478685,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "9c8e3a63-01a1-64b5-fe44-4f58f8350340",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "xmppCount",
"color": "#009688",
"settings": {},
"_hash": 0.5679285702280172,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "c6501413-d823-29c4-992f-9ae6e8e25549",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "ocppCount",
"color": "#795548",
"settings": {},
"_hash": 0.38390880166484287,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
},
{
"type": "entityCount",
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "2f04d6e5-8438-857a-ca78-ae78cc8b0c03",
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "customCount",
"color": "#00bcd4",
"settings": {},
"_hash": 0.9282529984749979,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
} }
], ],
"timewindow": { "timewindow": {
@ -2134,7 +2581,7 @@
"padding": "0px", "padding": "0px",
"settings": { "settings": {
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 6\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 7\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 8\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 9\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 10\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 11\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 12\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 13\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 14\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 15\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 16\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 17\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"!selectedTabIndex\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\r\n</div>\r\n", "markdownTextPattern": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\r\n <mat-tab-group [(selectedIndex)]=\"selectedTabIndex\">\r\n <mat-tab label=\"All\" value=\"gateway_devices_0\"></mat-tab>\r\n <mat-tab *ngIf=\"${mqttCount}\" label=\"MQTT\" value=\"gateway_devices_1\"></mat-tab>\r\n <mat-tab *ngIf=\"${modbusCount}\" label=\"MODBUS\" value=\"gateway_devices_2\"></mat-tab>\r\n <mat-tab *ngIf=\"${grpcCount}\" label=\"GRPC\" value=\"gateway_devices_3\"></mat-tab>\r\n <mat-tab *ngIf=\"${opcuaCount}\" label=\"OPCUA\" value=\"gateway_devices_4\"> </mat-tab>\r\n <mat-tab *ngIf=\"${bleCount}\" label=\"BLE\" value=\"gateway_devices_6\"></mat-tab>\r\n <mat-tab *ngIf=\"${requestCount}\" label=\"REQUEST\" value=\"gateway_devices_7\"></mat-tab>\r\n <mat-tab *ngIf=\"${canCount}\" label=\"CAN\" value=\"gateway_devices_8\"></mat-tab>\r\n <mat-tab *ngIf=\"${bacnetCount}\" label=\"BACNET\" value=\"gateway_devices_9\"></mat-tab>\r\n <mat-tab *ngIf=\"${odbcCount}\" label=\"ODBC\" value=\"gateway_devices_10\"></mat-tab>\r\n <mat-tab *ngIf=\"${restCount}\" label=\"REST\" value=\"gateway_devices_11\"></mat-tab>\r\n <mat-tab *ngIf=\"${snmpCount}\" label=\"SNMP\" value=\"gateway_devices_12\"></mat-tab>\r\n <mat-tab *ngIf=\"${ftpCount}\" label=\"FTP\" value=\"gateway_devices_13\"></mat-tab>\r\n <mat-tab *ngIf=\"${socketCount}\" label=\"SOCKET\" value=\"gateway_devices_14\"></mat-tab>\r\n <mat-tab *ngIf=\"${xmppCount}\" label=\"XMPP\" value=\"gateway_devices_15\"></mat-tab>\r\n <mat-tab *ngIf=\"${occpCount}\" label=\"OCCP\" value=\"gateway_devices_16\"></mat-tab>\r\n <mat-tab *ngIf=\"${customCount}\" label=\"CUSTOM\" value=\"gateway_devices_17\"></mat-tab>\r\n </mat-tab-group><tb-dashboard-state *ngIf=\"selectedTabIndex == 1\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_1\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 2\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_2\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 3\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_3\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 4\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_4\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 6\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 7\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 8\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 9\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 10\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 11\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 12\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 13\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 14\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 15\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 16\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 17\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"!selectedTabIndex\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\r\n</div>\r\n",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}" "markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}"
}, },

9
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -48,6 +48,7 @@ import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties; import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
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.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -62,7 +63,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse; import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse; import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse; import org.thingsboard.server.service.security.auth.rest.LoginResponse;
@ -79,7 +79,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j @Slf4j
@Configuration @Configuration
@TbCoreComponent @ConditionalOnExpression("('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && '${springdoc.api-docs.enabled:true}'=='true'")
@Profile("!test") @Profile("!test")
public class SwaggerConfiguration { public class SwaggerConfiguration {
@ -113,7 +113,8 @@ public class SwaggerConfiguration {
private String version; private String version;
@Value("${app.version:unknown}") @Value("${app.version:unknown}")
private String appVersion; private String appVersion;
@Value("${swagger.group_name:thingsboard}")
private String groupName;
@Bean @Bean
public OpenAPI thingsboardApi() { public OpenAPI thingsboardApi() {
@ -212,7 +213,7 @@ public class SwaggerConfiguration {
@Bean @Bean
public GroupedOpenApi groupedApi(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) { public GroupedOpenApi groupedApi(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
.group("thingsboard") .group(groupName)
.pathsToMatch(apiPath) .pathsToMatch(apiPath)
.addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer)) .addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer))
.addOperationCustomizer(operationCustomizer()) .addOperationCustomizer(operationCustomizer())

25
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -22,6 +22,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -359,29 +360,33 @@ public abstract class BaseController {
private ThingsboardException handleException(Exception exception, boolean logException) { private ThingsboardException handleException(Exception exception, boolean logException) {
if (logException && logControllerErrorStackTrace) { if (logException && logControllerErrorStackTrace) {
log.error("Error [{}]", exception.getMessage(), exception); try {
} SecurityUser user = getCurrentUser();
log.error("[{}][{}] Error", user.getTenantId(), user.getId(), exception);
String cause = ""; } catch (Exception e) {
if (exception.getCause() != null) { log.error("Error", exception);
cause = exception.getCause().getClass().getCanonicalName(); }
} }
Throwable cause = exception.getCause();
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause.contains("IncorrectParameterException")) { || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) { } else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) { } else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) { } else if (exception instanceof DataAccessException) {
String errorType = exception.getClass().getSimpleName();
if (!logControllerErrorStackTrace) { // not to log the error twice if (!logControllerErrorStackTrace) { // not to log the error twice
log.warn("Database error: {} - {}", errorType, ExceptionUtils.getRootCauseMessage(exception)); log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception));
}
if (cause instanceof ConstraintViolationException) {
return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
} }
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof EntityVersionMismatchException) { } else if (exception instanceof EntityVersionMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.CONFLICT); return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.CONFLICT);
} }

3
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -204,6 +204,9 @@ public class EdgeEventSourcingListener {
return false; return false;
} }
} }
if (entity instanceof OAuth2Info oAuth2Info) {
return oAuth2Info.isEdgeEnabled();
}
// Default: If the entity doesn't match any of the conditions, consider it as valid. // Default: If the entity doesn't match any of the conditions, consider it as valid.
return true; return true;
} }

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

@ -45,8 +45,11 @@ public class OAuth2EdgeEventFetcher implements EdgeEventFetcher {
@Override @Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = new ArrayList<>();
OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info(); OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info();
if (!oAuth2Info.isEdgeEnabled()) {
return new PageData<>();
}
List<EdgeEvent> result = new ArrayList<>();
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2, result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2,
EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info))); EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info)));
// returns PageData object to be in sync with other fetchers // returns PageData object to be in sync with other fetchers

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java

@ -40,7 +40,7 @@ public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) {
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class); OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class);
if (oAuth2Info != null) { if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) {
OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info); OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info);
downlinkMsg = DownlinkMsg.newBuilder() downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())

1
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -350,6 +350,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationUpdate update = NotificationUpdate.builder() NotificationUpdate update = NotificationUpdate.builder()
.updated(true) .updated(true)
.notificationId(notificationId.getId()) .notificationId(notificationId.getId())
.notificationType(notification.getType())
.newStatus(NotificationStatus.READ) .newStatus(NotificationStatus.READ)
.build(); .build();
onNotificationUpdate(tenantId, recipientId, update); onNotificationUpdate(tenantId, recipientId, update);

57
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -24,12 +24,14 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
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.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
@ -340,29 +342,33 @@ public class DefaultTbClusterService implements TbClusterService {
@Override @Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
TenantId tenantId = resource.getTenantId(); if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); TenantId tenantId = resource.getTenantId();
TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder()
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setResourceType(resource.getResourceType().name()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceKey(resource.getResourceKey()) .setResourceType(resource.getResourceType().name())
.build(); .setResourceKey(resource.getResourceKey())
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); .build();
broadcast(transportMsg, callback); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
} }
@Override @Override
public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) { public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) {
log.trace("[{}] Processing delete resource", resource); if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
TransportProtos.ResourceDeleteMsg resourceUpdateMsg = TransportProtos.ResourceDeleteMsg.newBuilder() log.trace("[{}][{}][{}] Processing delete resource", resource.getTenantId(), resource.getResourceType(), resource.getResourceKey());
.setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) TransportProtos.ResourceDeleteMsg resourceDeleteMsg = TransportProtos.ResourceDeleteMsg.newBuilder()
.setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits())
.setResourceType(resource.getResourceType().name()) .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits())
.setResourceKey(resource.getResourceKey()) .setResourceType(resource.getResourceType().name())
.build(); .setResourceKey(resource.getResourceKey())
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceUpdateMsg).build(); .build();
broadcast(transportMsg, callback); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
} }
private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
@ -384,8 +390,19 @@ public class DefaultTbClusterService implements TbClusterService {
} }
private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) { private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) {
TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
Set<String> tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT); Set<String> tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT);
broadcast(transportMsg, tbTransportServices, callback);
}
private void broadcast(ToTransportMsg transportMsg, String transportType, TbQueueCallback callback) {
Set<String> tbTransportServices = partitionService.getAllServices(ServiceType.TB_TRANSPORT).stream()
.filter(info -> info.getTransportsList().contains(transportType))
.map(TransportProtos.ServiceInfo::getServiceId).collect(Collectors.toSet());
broadcast(transportMsg, tbTransportServices, callback);
}
private void broadcast(ToTransportMsg transportMsg, Set<String> tbTransportServices, TbQueueCallback callback) {
TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null; TbQueueCallback proxyCallback = callback != null ? new MultipleTbQueueCallbackWrapper(tbTransportServices.size(), callback) : null;
for (String transportServiceId : tbTransportServices) { for (String transportServiceId : tbTransportServices) {
TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId); TopicPartitionInfo tpi = topicService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId);

2
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java

@ -21,7 +21,6 @@ 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 java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@Data @Data
@ -52,4 +51,5 @@ public abstract class TbSubscription<T> {
public int hashCode() { public int hashCode() {
return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type);
} }
} }

11
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
@ -78,6 +79,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
.entityId(securityCtx.getId()) .entityId(securityCtx.getId())
.updateProcessor(this::handleNotificationsSubscriptionUpdate) .updateProcessor(this::handleNotificationsSubscriptionUpdate)
.limit(cmd.getLimit()) .limit(cmd.getLimit())
.notificationTypes(cmd.getTypes())
.build(); .build();
localSubscriptionService.addSubscription(subscription); localSubscriptionService.addSubscription(subscription);
@ -105,8 +107,8 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotifications(NotificationsSubscription subscription) { private void fetchUnreadNotifications(NotificationsSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId()); log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId());
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(), PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientIdAndNotificationTypes(subscription.getTenantId(),
WEB, (UserId) subscription.getEntityId(), subscription.getLimit()); WEB, (UserId) subscription.getEntityId(), subscription.getNotificationTypes(), subscription.getLimit());
subscription.getLatestUnreadNotifications().clear(); subscription.getLatestUnreadNotifications().clear();
notifications.getData().forEach(notification -> { notifications.getData().forEach(notification -> {
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification);
@ -139,6 +141,11 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
Notification notification = update.getNotification(); Notification notification = update.getNotification();
UUID notificationId = notification != null ? notification.getUuidId() : update.getNotificationId(); UUID notificationId = notification != null ? notification.getUuidId() : update.getNotificationId();
NotificationType notificationType = notification != null ? notification.getType() : update.getNotificationType();
if (notificationType != null && !subscription.checkNotificationType(notificationType)) {
return;
}
if (update.isCreated()) { if (update.isCreated()) {
subscription.getLatestUnreadNotifications().put(notificationId, notification); subscription.getLatestUnreadNotifications().put(notificationId, notification);
subscription.getTotalUnreadCounter().incrementAndGet(); subscription.getTotalUnreadCounter().incrementAndGet();

4
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java

@ -18,15 +18,19 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.service.ws.WsCmd; import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType; import org.thingsboard.server.service.ws.WsCmdType;
import java.util.Set;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class NotificationsSubCmd implements WsCmd { public class NotificationsSubCmd implements WsCmd {
private int cmdId; private int cmdId;
private int limit; private int limit;
private Set<NotificationType> types;
@Override @Override
public WsCmdType getType() { public WsCmdType getType() {

6
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java

@ -24,13 +24,13 @@ import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType; import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType;
import java.util.Collection; import java.util.List;
@Getter @Getter
@ToString(exclude = "notifications") @ToString(exclude = "notifications")
public class UnreadNotificationsUpdate extends CmdUpdate { public class UnreadNotificationsUpdate extends CmdUpdate {
private final Collection<Notification> notifications; private final List<Notification> notifications;
private final Notification update; private final Notification update;
private final int totalUnreadCount; private final int totalUnreadCount;
private final int sequenceNumber; private final int sequenceNumber;
@ -39,7 +39,7 @@ public class UnreadNotificationsUpdate extends CmdUpdate {
@JsonCreator @JsonCreator
public UnreadNotificationsUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode, public UnreadNotificationsUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
@JsonProperty("errorMsg") String errorMsg, @JsonProperty("errorMsg") String errorMsg,
@JsonProperty("notifications") Collection<Notification> notifications, @JsonProperty("notifications") List<Notification> notifications,
@JsonProperty("update") Notification update, @JsonProperty("update") Notification update,
@JsonProperty("totalUnreadCount") int totalUnreadCount, @JsonProperty("totalUnreadCount") int totalUnreadCount,
@JsonProperty("sequenceNumber") int sequenceNumber) { @JsonProperty("sequenceNumber") int sequenceNumber) {

2
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java

@ -21,6 +21,7 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import java.util.UUID; import java.util.UUID;
@ -31,6 +32,7 @@ import java.util.UUID;
public class NotificationUpdate { public class NotificationUpdate {
private UUID notificationId; private UUID notificationId;
private NotificationType notificationType;
private boolean created; private boolean created;
private Notification notification; private Notification notification;

1
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java

@ -23,7 +23,6 @@ import org.thingsboard.server.service.subscription.TbSubscription;
import org.thingsboard.server.service.subscription.TbSubscriptionType; import org.thingsboard.server.service.subscription.TbSubscriptionType;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@Getter @Getter

12
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java

@ -17,10 +17,12 @@ package org.thingsboard.server.service.ws.notification.sub;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.collections4.CollectionUtils;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.EntityId; 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.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.service.subscription.TbSubscription; import org.thingsboard.server.service.subscription.TbSubscription;
import org.thingsboard.server.service.subscription.TbSubscriptionType; import org.thingsboard.server.service.subscription.TbSubscriptionType;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
@ -29,8 +31,8 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -39,13 +41,19 @@ public class NotificationsSubscription extends AbstractNotificationSubscription<
private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>(); private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>();
private final int limit; private final int limit;
private final Set<NotificationType> notificationTypes;
@Builder @Builder
public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
BiConsumer<TbSubscription<NotificationsSubscriptionUpdate>, NotificationsSubscriptionUpdate> updateProcessor, BiConsumer<TbSubscription<NotificationsSubscriptionUpdate>, NotificationsSubscriptionUpdate> updateProcessor,
int limit) { int limit, Set<NotificationType> notificationTypes) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS, updateProcessor); super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS, updateProcessor);
this.limit = limit; this.limit = limit;
this.notificationTypes = notificationTypes;
}
public boolean checkNotificationType(NotificationType type) {
return CollectionUtils.isEmpty(notificationTypes) || notificationTypes.contains(type);
} }
public UnreadNotificationsUpdate createFullUpdate() { public UnreadNotificationsUpdate createFullUpdate() {

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

@ -1392,6 +1392,8 @@ swagger:
url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}" url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}"
# The version of the API doc to display. Default to the package version. # The version of the API doc to display. Default to the package version.
version: "${SWAGGER_VERSION:}" version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
# Queue configuration parameters # Queue configuration parameters
queue: queue:

31
application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java

@ -27,13 +27,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
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;
@ -48,6 +51,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -547,6 +551,33 @@ public class DashboardControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(dashboardDao, savedTenant.getId(), dashboardId, "/api/dashboard/" + dashboardId); testEntityDaoWithRelationsTransactionalException(dashboardDao, savedTenant.getId(), dashboardId, "/api/dashboard/" + dashboardId);
} }
@Test
public void whenDeletingDashboard_ifReferencedByDeviceProfile_thenReturnError() throws Exception {
Dashboard dashboard = createDashboard("test");
DeviceProfile deviceProfile = createDeviceProfile("test");
deviceProfile.setDefaultDashboardId(dashboard.getId());
doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
String response = doDelete("/api/dashboard/" + dashboard.getUuidId()).andExpect(status().isBadRequest())
.andReturn().getResponse().getContentAsString();
String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText();
assertThat(errorMessage).containsIgnoringCase("referenced by a device profile");
}
@Test
public void whenDeletingDashboard_ifReferencedByAssetProfile_thenReturnError() throws Exception {
Dashboard dashboard = createDashboard("test");
AssetProfile assetProfile = createAssetProfile("test");
assetProfile.setDefaultDashboardId(dashboard.getId());
doPost("/api/assetProfile", assetProfile, AssetProfile.class);
String response = doDelete("/api/dashboard/" + dashboard.getUuidId()).andExpect(status().isBadRequest())
.andReturn().getResponse().getContentAsString();
String errorMessage = JacksonUtil.toJsonNode(response).get("message").asText();
assertThat(errorMessage).containsIgnoringCase("referenced by an asset profile");
}
private Dashboard createDashboard(String title) { private Dashboard createDashboard(String title) {
Dashboard dashboard = new Dashboard(); Dashboard dashboard = new Dashboard();
dashboard.setTitle(title); dashboard.setTitle(title);

9
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -616,6 +616,14 @@ public class TenantControllerTest extends AbstractControllerTest {
assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC); assertThat(usedTpi.getTopic()).isEqualTo(DataConstants.HP_QUEUE_TOPIC);
assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID); assertThat(usedTpi.getTenantId()).get().isEqualTo(TenantId.SYS_TENANT_ID);
}); });
assertThat(partitionService.resolve(ServiceType.TB_RULE_ENGINE, null, tenantId, tenantId)).satisfies(tpi -> {
assertThat(tpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC);
assertThat(tpi.getTenantId()).get().isEqualTo(tenantId);
});
assertThat(partitionService.resolve(ServiceType.TB_RULE_ENGINE, "", tenantId, tenantId)).satisfies(tpi -> {
assertThat(tpi.getTopic()).isEqualTo(MAIN_QUEUE_TOPIC);
assertThat(tpi.getTenantId()).get().isEqualTo(tenantId);
});
loginSysAdmin(); loginSysAdmin();
tenantProfile.setIsolatedTbRuleEngine(true); tenantProfile.setIsolatedTbRuleEngine(true);
@ -850,4 +858,5 @@ public class TenantControllerTest extends AbstractControllerTest {
testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant())); testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant()));
Mockito.reset(tbClusterService); Mockito.reset(tbClusterService);
} }
} }

1
application/src/test/java/org/thingsboard/server/edge/NotificationEdgeTest.java

@ -18,7 +18,6 @@ package org.thingsboard.server.edge;
import com.google.protobuf.AbstractMessage; import com.google.protobuf.AbstractMessage;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;

7
application/src/test/java/org/thingsboard/server/edge/OAuth2EdgeTest.java

@ -59,12 +59,7 @@ public class OAuth2EdgeTest extends AbstractEdgeTest {
oAuth2Info.setEnabled(false); oAuth2Info.setEnabled(false);
oAuth2Info.setEdgeEnabled(false); oAuth2Info.setEdgeEnabled(false);
doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class); doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertFalse(edgeImitator.waitForMessages(5));
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg);
oAuth2UpdateMsg = (OAuth2UpdateMsg) latestMessage;
result = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true);
Assert.assertEquals(oAuth2Info, result);
edgeImitator.ignoreType(OAuth2UpdateMsg.class); edgeImitator.ignoreType(OAuth2UpdateMsg.class);
loginTenantAdmin(); loginTenantAdmin();

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

@ -143,6 +143,14 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
return submitNotificationRequest(targetId, text, 0, deliveryMethods); return submitNotificationRequest(targetId, text, 0, deliveryMethods);
} }
protected NotificationRequest submitNotificationRequest(NotificationType type, NotificationTargetId targetId, String text, NotificationDeliveryMethod... deliveryMethods) {
if (deliveryMethods.length == 0) {
deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB};
}
NotificationTemplate notificationTemplate = createNotificationTemplate(type, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods);
return submitNotificationRequest(List.of(targetId), notificationTemplate.getId(), 0);
}
protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) { protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) {
return submitNotificationRequest(List.of(targetId), text, delayInSec, deliveryMethods); return submitNotificationRequest(List.of(targetId), text, delayInSec, deliveryMethods);
} }

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

@ -210,6 +210,88 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
checkPartialNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText, 1); checkPartialNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText, 1);
} }
@Test
public void testNotificationUpdates_typesFilter_multipleSubs() {
int generalSub = wsClient.subscribeForUnreadNotificationsAndWait(10, NotificationType.GENERAL);
int alarmSub = wsClient.subscribeForUnreadNotificationsAndWait(10, NotificationType.ALARM, NotificationType.GENERAL);
int entityActionSub = wsClient.subscribeForUnreadNotificationsAndWait(10, NotificationType.ENTITY_ACTION, NotificationType.GENERAL);
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String generalNotificationText1 = "General notification 1";
submitNotificationRequest(NotificationType.GENERAL, notificationTarget.getId(), generalNotificationText1);
// expecting all 3 subs to received update
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKeys(generalSub, alarmSub, entityActionSub)
.allMatch(update -> update.getUpdate().getText().equals(generalNotificationText1)
&& update.getTotalUnreadCount() == 1);
});
Notification generalNotification1 = wsClient.getLastDataUpdate().getUpdate();
String generalNotificationText2 = "General notification 2";
submitNotificationRequest(NotificationType.GENERAL, notificationTarget.getId(), generalNotificationText2);
// expecting all 3 subs to received update
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKeys(generalSub, alarmSub, entityActionSub)
.allMatch(update -> update.getUpdate().getText().equals(generalNotificationText2)
&& update.getTotalUnreadCount() == 2);
});
Notification generalNotification2 = wsClient.getLastDataUpdate().getUpdate();
// marking as read, expecting all 3 subs to received update
wsClient.markNotificationAsRead(generalNotification1.getUuidId());
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKeys(generalSub, alarmSub, entityActionSub)
.allMatch(update -> update.getTotalUnreadCount() == 1 && update.getNotifications().size() == 1
&& update.getNotifications().get(0).getText().equals(generalNotificationText2));
});
wsClient.getLastUpdates().clear();
String alarmNotificationText1 = "Alarm notification 1";
submitNotificationRequest(NotificationType.ALARM, notificationTarget.getId(), alarmNotificationText1);
// expecting only 1 sub to received update
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKey(alarmSub)
.matches(update -> update.getUpdate().getText().equals(alarmNotificationText1)
&& update.getTotalUnreadCount() == 2);
});
Notification alarmNotification1 = wsClient.getLastDataUpdate().getUpdate();
String alarmNotificationText2 = "Alarm notification 2";
submitNotificationRequest(NotificationType.ALARM, notificationTarget.getId(), alarmNotificationText2);
// expecting only 1 sub to received update
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKey(alarmSub)
.matches(update -> update.getUpdate().getText().equals(alarmNotificationText2)
&& update.getTotalUnreadCount() == 3);
});
await().during(3, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKeys(generalSub, entityActionSub)
.containsOnlyNulls();
});
// marking as read, expecting only 1 sub to receive update
wsClient.markNotificationAsRead(alarmNotification1.getUuidId());
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKey(alarmSub)
.matches(update -> update.getTotalUnreadCount() == 2 && update.getNotifications().size() == 2);
});
await().during(3, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKeys(generalSub, entityActionSub)
.containsOnlyNulls();
});
// marking as read, expecting general and entity action subs with 0 unread, and alarm with 1 unread
wsClient.markNotificationAsRead(generalNotification2.getUuidId());
await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(wsClient.getLastUpdates()).extractingByKeys(generalSub, entityActionSub)
.allMatch(update -> update.getTotalUnreadCount() == 0 && update.getNotifications().isEmpty());
assertThat(wsClient.getLastUpdates()).extractingByKey(alarmSub)
.matches(update -> update.getTotalUnreadCount() == 1 && update.getNotifications().size() == 1
&& update.getNotifications().get(0).getText().equals(alarmNotificationText2));
});
}
@Test @Test
public void testMarkingAsRead_multipleSessions() throws Exception { public void testMarkingAsRead_multipleSessions() throws Exception {
connectOtherWsClient(); connectOtherWsClient();

23
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.RandomUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.controller.TbTestWebSocketClient; import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
@ -35,7 +36,10 @@ import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Getter @Getter
@ -48,18 +52,28 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
private int unreadCount; private int unreadCount;
private List<Notification> notifications; private List<Notification> notifications;
private final Map<Integer, UnreadNotificationsUpdate> lastUpdates = new ConcurrentHashMap<>();
public NotificationApiWsClient(String wsUrl) throws URISyntaxException { public NotificationApiWsClient(String wsUrl) throws URISyntaxException {
super(new URI(wsUrl + "/api/ws")); super(new URI(wsUrl + "/api/ws"));
} }
public NotificationApiWsClient subscribeForUnreadNotifications(int limit) { public NotificationApiWsClient subscribeForUnreadNotifications(int limit, NotificationType... types) {
send(new NotificationsSubCmd(1, limit)); send(new NotificationsSubCmd(newCmdId(), limit, Arrays.stream(types).collect(Collectors.toSet())));
this.limit = limit; this.limit = limit;
return this; return this;
} }
public int subscribeForUnreadNotificationsAndWait(int limit, NotificationType... types) {
int subId = newCmdId();
send(new NotificationsSubCmd(subId, limit, Arrays.stream(types).collect(Collectors.toSet())));
waitForReply();
this.limit = limit;
return subId;
}
public NotificationApiWsClient subscribeForUnreadNotificationsCount() { public NotificationApiWsClient subscribeForUnreadNotificationsCount() {
send(new NotificationsCountSubCmd(2)); send(new NotificationsCountSubCmd(newCmdId()));
return this; return this;
} }
@ -84,6 +98,7 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
CmdUpdateType updateType = CmdUpdateType.valueOf(update.get("cmdUpdateType").asText()); CmdUpdateType updateType = CmdUpdateType.valueOf(update.get("cmdUpdateType").asText());
if (updateType == CmdUpdateType.NOTIFICATIONS) { if (updateType == CmdUpdateType.NOTIFICATIONS) {
lastDataUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsUpdate.class); lastDataUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsUpdate.class);
lastUpdates.put(lastDataUpdate.getCmdId(), lastDataUpdate);
unreadCount = lastDataUpdate.getTotalUnreadCount(); unreadCount = lastDataUpdate.getTotalUnreadCount();
if (lastDataUpdate.getNotifications() != null) { if (lastDataUpdate.getNotifications() != null) {
notifications = new ArrayList<>(lastDataUpdate.getNotifications()); notifications = new ArrayList<>(lastDataUpdate.getNotifications());
@ -115,7 +130,7 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
super.onMessage(s); super.onMessage(s);
} }
private static int newCmdId() { private int newCmdId() {
return RandomUtils.nextInt(1, 1000); return RandomUtils.nextInt(1, 1000);
} }

5
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java

@ -19,10 +19,13 @@ import org.thingsboard.server.common.data.id.NotificationId;
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.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
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 java.util.Set;
public interface NotificationService { public interface NotificationService {
Notification saveNotification(TenantId tenantId, Notification notification); Notification saveNotification(TenantId tenantId, Notification notification);
@ -35,7 +38,7 @@ public interface NotificationService {
PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink); PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink);
PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit); PageData<Notification> findLatestUnreadNotificationsByRecipientIdAndNotificationTypes(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, Set<NotificationType> types, int limit);
int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId); int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId);

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.ToString;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -53,9 +54,12 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques
private long startTs; private long startTs;
@Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") @Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522")
private long endTs; private long endTs;
@ToString.Exclude
@NoXss @NoXss
@Schema(description = "JSON object with alarm details") @Schema(description = "JSON object with alarm details")
private JsonNode details; private JsonNode details;
@Valid @Valid
@Schema(description = "JSON object with propagation details") @Schema(description = "JSON object with propagation details")
private AlarmPropagationInfo propagation; private AlarmPropagationInfo propagation;

4
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.ToString;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
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;
@ -47,9 +48,12 @@ public class AlarmUpdateRequest implements AlarmModificationRequest {
private long startTs; private long startTs;
@Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") @Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522")
private long endTs; private long endTs;
@ToString.Exclude
@NoXss @NoXss
@Schema(description = "JSON object with alarm details") @Schema(description = "JSON object with alarm details")
private JsonNode details; private JsonNode details;
@Valid @Valid
@Schema(description = "JSON object with propagation details") @Schema(description = "JSON object with propagation details")
private AlarmPropagationInfo propagation; private AlarmPropagationInfo propagation;

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -32,4 +32,5 @@ public enum NotificationType {
EDGE_CONNECTION, EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE, EDGE_COMMUNICATION_FAILURE,
TASK_PROCESSING_FAILURE TASK_PROCESSING_FAILURE
} }

5
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.queue.discovery;
import com.google.common.hash.HashFunction; import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import jakarta.annotation.PostConstruct;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -36,7 +37,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent; import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -319,7 +319,7 @@ public class HashPartitionService implements PartitionService {
private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) { private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) {
TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId); TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId);
if (queueName == null) { if (queueName == null || queueName.isEmpty()) {
queueName = MAIN_QUEUE_NAME; queueName = MAIN_QUEUE_NAME;
} }
QueueKey queueKey = new QueueKey(serviceType, queueName, isolatedOrSystemTenantId); QueueKey queueKey = new QueueKey(serviceType, queueName, isolatedOrSystemTenantId);
@ -672,6 +672,7 @@ public class HashPartitionService implements PartitionService {
public QueueConfig(QueueRoutingInfo queueRoutingInfo) { public QueueConfig(QueueRoutingInfo queueRoutingInfo) {
this.duplicateMsgToAllPartitions = queueRoutingInfo.isDuplicateMsgToAllPartitions(); this.duplicateMsgToAllPartitions = queueRoutingInfo.isDuplicateMsgToAllPartitions();
} }
} }
} }

516
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -46,11 +46,23 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import static java.lang.Character.MAX_RADIX;
import static java.lang.Character.MIN_RADIX;
@Slf4j @Slf4j
public class TbUtils { public class TbUtils {
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private static final int ZERO_RADIX = 0;
private static final int OCTAL_RADIX = 8;
private static final int DEC_RADIX = 10;
private static final int HEX_RADIX = 16;
private static final int HEX_LEN_MIN = -1;
private static final int HEX_LEN_INT_MAX = 8;
private static final int HEX_LEN_LONG_MAX = 16;
private static final int BYTES_LEN_LONG_MAX = 8;
private static final LinkedHashMap<String, String> mdnEncodingReplacements = new LinkedHashMap<>(); private static final LinkedHashMap<String, String> mdnEncodingReplacements = new LinkedHashMap<>();
static { static {
@ -103,6 +115,8 @@ public class TbUtils {
String.class, int.class))); String.class, int.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat", parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class))); String.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class, int.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble", parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
String.class))); String.class)));
parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt", parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
@ -175,6 +189,40 @@ public class TbUtils {
float.class, int.class))); float.class, int.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class, boolean.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class, boolean.class, boolean.class)));
parserConfig.addImport("intToHex", new MethodStub(TbUtils.class.getMethod("intToHex",
Integer.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class, boolean.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class, boolean.class, boolean.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class, boolean.class)));
parserConfig.addImport("doubleToHex", new MethodStub(TbUtils.class.getMethod("doubleToHex",
Double.class)));
parserConfig.addImport("doubleToHex", new MethodStub(TbUtils.class.getMethod("doubleToHex",
Double.class, boolean.class)));
parserConfig.addImport("printUnsignedBytes", new MethodStub(TbUtils.class.getMethod("printUnsignedBytes",
ExecutionContext.class, List.class)));
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex", parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
String.class))); String.class)));
parserConfig.addImport("base64ToBytes", new MethodStub(TbUtils.class.getMethod("base64ToBytes", parserConfig.addImport("base64ToBytes", new MethodStub(TbUtils.class.getMethod("base64ToBytes",
@ -197,6 +245,18 @@ public class TbUtils {
String.class))); String.class)));
parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI", parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI",
String.class))); String.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class, Object.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class)));
parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary",
String.class)));
parserConfig.addImport("isOctal", new MethodStub(TbUtils.class.getMethod("isOctal",
String.class)));
parserConfig.addImport("isDecimal", new MethodStub(TbUtils.class.getMethod("isDecimal",
String.class)));
parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal",
String.class)));
} }
public static String btoa(String input) { public static String btoa(String input) {
@ -210,6 +270,7 @@ public class TbUtils {
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException { public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList)); return TbJson.parse(ctx, bytesToString(bytesList));
} }
public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException { public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException {
return TbJson.parse(ctx, jsonStr); return TbJson.parse(ctx, jsonStr);
} }
@ -269,88 +330,127 @@ public class TbUtils {
} }
public static Integer parseInt(String value) { public static Integer parseInt(String value) {
int radix = getRadix(value); return parseInt(value, ZERO_RADIX);
return parseInt(value, radix);
} }
public static Integer parseInt(String value, int radix) { public static Integer parseInt(String value, int radix) {
if (StringUtils.isNotBlank(value)) { if (StringUtils.isNotBlank(value)) {
try { String valueP = prepareNumberString(value);
String valueP = prepareNumberString(value); int radixValue = isValidStringAndRadix(valueP, radix, value);
isValidRadix(valueP, radix); if (radixValue >= 25 && radixValue <= MAX_RADIX) {
try { return (Integer) compareIntLongValueMinMax(valueP, radixValue, Integer.MAX_VALUE, Integer.MIN_VALUE);
return Integer.parseInt(valueP, radix);
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
if (bi.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0)
throw new NumberFormatException("Value \"" + value + "\" is greater than the maximum Integer value " + Integer.MAX_VALUE + " !");
if (bi.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0)
throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Integer value " + Integer.MIN_VALUE + " !");
Float f = parseFloat(valueP);
if (f != null) {
return f.intValue();
} else {
throw new NumberFormatException(e.getMessage());
}
}
} catch (NumberFormatException e) {
throw new NumberFormatException(e.getMessage());
} }
return switch (radixValue) {
case MIN_RADIX -> parseBinaryStringAsSignedInteger(valueP);
case OCTAL_RADIX, DEC_RADIX, HEX_RADIX -> Integer.parseInt(valueP, radixValue);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
} }
return null; return null;
} }
public static Long parseLong(String value) { public static Long parseLong(String value) {
int radix = getRadix(value); return parseLong(value, ZERO_RADIX);
return parseLong(value, radix);
} }
public static Long parseLong(String value, int radix) { public static Long parseLong(String value, int radix) {
if (StringUtils.isNotBlank(value)) { if (StringUtils.isNotBlank(value)) {
try { String valueP = prepareNumberString(value);
String valueP = prepareNumberString(value); int radixValue = isValidStringAndRadix(valueP, radix, value);
isValidRadix(valueP, radix); if (radixValue >= 25 && radixValue <= MAX_RADIX) {
try { return (Long) compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE);
return Long.parseLong(valueP, radix);
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
if (bi.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0)
throw new NumberFormatException("Value \"" + value + "\"is greater than the maximum Long value " + Long.MAX_VALUE + " !");
if (bi.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0)
throw new NumberFormatException("Value \"" + value + "\" is less than the minimum Long value " + Long.MIN_VALUE + " !");
Double dd = parseDouble(valueP);
if (dd != null) {
return dd.longValue();
} else {
throw new NumberFormatException(e.getMessage());
}
}
} catch (NumberFormatException e) {
throw new NumberFormatException(e.getMessage());
} }
return switch (radixValue) {
case MIN_RADIX -> parseBinaryStringAsSignedLong(valueP);
case OCTAL_RADIX, DEC_RADIX, HEX_RADIX -> Long.parseLong(valueP, radixValue);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
} }
return null; return null;
} }
private static int getRadix(String value, int... radixS) { private static int parseBinaryStringAsSignedInteger(String binaryString) {
return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10; if (binaryString.length() != 32) {
// Pad the binary string to 64 bits if it is not already
binaryString = String.format("%32s", binaryString).replace(' ', '0');
}
// If the MSB is 1, the number is negative in two's complement
if (binaryString.charAt(0) == '1') {
// Calculate the two's complement
String invertedBinaryString = invertBinaryString(binaryString);
int positiveValue = Integer.parseInt(invertedBinaryString, MIN_RADIX) + 1;
return -positiveValue;
} else {
return Integer.parseInt(binaryString, MIN_RADIX);
}
}
private static long parseBinaryStringAsSignedLong(String binaryString) {
if (binaryString.length() != 64) {
// Pad the binary string to 64 bits if it is not already
binaryString = String.format("%64s", binaryString).replace(' ', '0');
}
// If the MSB is 1, the number is negative in two's complement
if (binaryString.charAt(0) == '1') {
// Calculate the two's complement
String invertedBinaryString = invertBinaryString(binaryString);
long positiveValue = Long.parseLong(invertedBinaryString, MIN_RADIX) + 1;
return -positiveValue;
} else {
return Long.parseLong(binaryString, MIN_RADIX);
}
}
private static String invertBinaryString(String binaryString) {
StringBuilder invertedString = new StringBuilder();
for (char bit : binaryString.toCharArray()) {
invertedString.append(bit == '0' ? '1' : '0');
}
return invertedString.toString();
}
private static int getRadix10_16(String value) {
int radix = isDecimal(value) > 0 ? DEC_RADIX : isHexadecimal(value);
if (radix > 0) {
return radix;
} else {
throw new NumberFormatException("Value: \"" + value + "\" is not numeric or hexDecimal format!");
}
} }
public static Float parseFloat(String value) { public static Float parseFloat(String value) {
if (value != null) { return parseFloat(value, 0);
try { }
return Float.parseFloat(prepareNumberString(value));
} catch (NumberFormatException e) { public static Float parseFloat(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Float.parseFloat(value);
} else {
int bits = Integer.parseUnsignedInt(valueP, HEX_RADIX);
return Float.intBitsToFloat(bits);
} }
} }
return null; return null;
} }
public static Double parseDouble(String value) { public static Double parseDouble(String value) {
int radix = getRadix10_16(value);
return parseDouble(value, radix);
}
public static Double parseDouble(String value, int radix) {
if (value != null) { if (value != null) {
try { String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Double.parseDouble(prepareNumberString(value)); return Double.parseDouble(prepareNumberString(value));
} catch (NumberFormatException e) { } else {
long bits = Long.parseUnsignedLong(valueP, HEX_RADIX);
return Double.longBitsToDouble(bits);
} }
} }
return null; return null;
@ -368,9 +468,10 @@ public class TbUtils {
return parseHexToInt(hex, true); return parseHexToInt(hex, true);
} }
public static int parseHexToInt(String hex, boolean bigEndian) { public static Integer parseHexToInt(String value, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8); String hexValue = prepareNumberString(value);
return parseBytesToInt(data, 0, data.length, bigEndian); String hex = bigEndian ? hexValue : reverseHexStringByOrder(hexValue);
return parseInt(hex, HEX_RADIX);
} }
public static long parseLittleEndianHexToLong(String hex) { public static long parseLittleEndianHexToLong(String hex) {
@ -385,9 +486,10 @@ public class TbUtils {
return parseHexToLong(hex, true); return parseHexToLong(hex, true);
} }
public static long parseHexToLong(String hex, boolean bigEndian) { public static Long parseHexToLong(String value, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16); String hexValue = prepareNumberString(value);
return parseBytesToLong(data, 0, data.length, bigEndian); String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseLong(hex, HEX_RADIX);
} }
public static float parseLittleEndianHexToFloat(String hex) { public static float parseLittleEndianHexToFloat(String hex) {
@ -402,9 +504,10 @@ public class TbUtils {
return parseHexToFloat(hex, true); return parseHexToFloat(hex, true);
} }
public static float parseHexToFloat(String hex, boolean bigEndian) { public static Float parseHexToFloat(String value, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8); String hexValue = prepareNumberString(value);
return parseBytesToFloat(data, 0, bigEndian); String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseFloat(hex, HEX_RADIX);
} }
public static double parseLittleEndianHexToDouble(String hex) { public static double parseLittleEndianHexToDouble(String hex) {
@ -419,39 +522,171 @@ public class TbUtils {
return parseHexToDouble(hex, true); return parseHexToDouble(hex, true);
} }
public static double parseHexToDouble(String hex, boolean bigEndian) { public static double parseHexToDouble(String value, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16); String hexValue = prepareNumberString(value);
return parseBytesToDouble(data, 0, bigEndian); String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseDouble(hex, HEX_RADIX);
} }
private static byte[] prepareHexToBytesNumber(String hex, int len) { public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String value) {
int length = hex.length(); String hex = prepareNumberString(value);
if (length > len) { int len = hex.length();
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed."); if (len % 2 > 0) {
}
if (length % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length."); throw new IllegalArgumentException("Hex string must be even-length.");
} }
byte[] data = new byte[length / 2]; ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < length; i += 2) { for (int i = 0; i < hex.length(); i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16)); // Extract two characters from the hex string
String byteString = hex.substring(i, i + 2);
// Parse the hex string to a byte
byte byteValue = (byte) Integer.parseInt(byteString, HEX_RADIX);
// Add the byte to the ArrayList
data.add(byteValue);
} }
return data; return data;
} }
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) { public static List<Integer> printUnsignedBytes(ExecutionContext ctx, List<Byte> byteArray) {
int len = hex.length(); ExecutionArrayList<Integer> data = new ExecutionArrayList<>(ctx);
if (len % 2 > 0) { for (Byte b : byteArray) {
throw new IllegalArgumentException("Hex string must be even-length."); // Convert signed byte to unsigned integer
} int unsignedByte = Byte.toUnsignedInt(b);
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx); data.add(unsignedByte);
for (int i = 0; i < len; i += 2) {
data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16)));
} }
return data; return data;
} }
public static String intToHex(Integer i) {
return prepareNumberHexString(i.longValue(), true, false, HEX_LEN_MIN, HEX_LEN_INT_MAX);
}
public static String intToHex(Integer i, boolean bigEndian) {
return prepareNumberHexString(i.longValue(), bigEndian, false, HEX_LEN_MIN, HEX_LEN_INT_MAX);
}
public static String intToHex(Integer i, boolean bigEndian, boolean pref) {
return prepareNumberHexString(i.longValue(), bigEndian, pref, HEX_LEN_MIN, HEX_LEN_INT_MAX);
}
public static String intToHex(Integer i, boolean bigEndian, boolean pref, int len) {
return prepareNumberHexString(i.longValue(), bigEndian, pref, len, HEX_LEN_INT_MAX);
}
public static String longToHex(Long l) {
return prepareNumberHexString(l, true, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian) {
return prepareNumberHexString(l, bigEndian, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian, boolean pref) {
return prepareNumberHexString(l, bigEndian, pref, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian, boolean pref, int len) {
return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX);
}
public static String intLongToString(Long number) {
return intLongToString(number, DEC_RADIX);
}
public static String intLongToString(Long number, int radix) {
return intLongToString(number, radix, true);
}
public static String intLongToString(Long number, int radix, boolean bigEndian) {
return intLongToString(number, radix, bigEndian, false);
}
public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) {
if (radix >= 25 && radix <= MAX_RADIX) {
return Long.toString(number, radix);
}
return switch (radix) {
case MIN_RADIX -> Long.toBinaryString(number);
case OCTAL_RADIX -> Long.toOctalString(number);
case DEC_RADIX -> Long.toString(number);
case HEX_RADIX -> prepareNumberHexString(number, bigEndian, pref, -1, -1);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
}
private static Number compareIntLongValueMinMax(String valueP, int radix, Number maxValue, Number minValue) {
boolean isInteger = maxValue.getClass().getSimpleName().equals("Integer");
try {
if (isInteger) {
return Integer.parseInt(valueP, radix);
} else {
return Long.parseLong(valueP, radix);
}
} catch (NumberFormatException e) {
BigInteger bi = new BigInteger(valueP, radix);
long maxValueL = isInteger ? maxValue.longValue() : (long) maxValue;
if (bi.compareTo(BigInteger.valueOf(maxValueL)) > 0) {
throw new NumberFormatException("Value \"" + valueP + "\"is greater than the maximum " + maxValue.getClass().getSimpleName() + " value " + maxValue + " !");
}
long minValueL = isInteger ? minValue.longValue() : (long) minValue;
if (bi.compareTo(BigInteger.valueOf(minValueL)) < 0) {
throw new NumberFormatException("Value \"" + valueP + "\" is less than the minimum " + minValue.getClass().getSimpleName() + " value " + minValue + " !");
}
throw new NumberFormatException(e.getMessage());
}
}
private static String prepareNumberHexString(Long number, boolean bigEndian, boolean pref, int len, int hexLenMax) {
String hex = Long.toHexString(number).toUpperCase();
hexLenMax = hexLenMax < 0 ? hex.length() : hexLenMax;
String hexWithoutZeroFF = removeLeadingZero_FF(hex, number, hexLenMax);
hexWithoutZeroFF = bigEndian ? hexWithoutZeroFF : reverseHexStringByOrder(hexWithoutZeroFF);
len = len == HEX_LEN_MIN ? hexWithoutZeroFF.length() : len;
String result = hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - len);
return pref ? "0x" + result : result;
}
private static String removeLeadingZero_FF(String hex, Long number, int hexLenMax) {
String hexWithoutZero = hex.replaceFirst("^0+(?!$)", ""); // Remove leading zeros except for the last one
hexWithoutZero = hexWithoutZero.length() % 2 > 0 ? "0" + hexWithoutZero : hexWithoutZero;
if (number >= 0) {
return hexWithoutZero;
} else {
String hexWithoutZeroFF = hexWithoutZero.replaceFirst("^F+(?!$)", "");
hexWithoutZeroFF = hexWithoutZeroFF.length() % 2 > 0 ? "F" + hexWithoutZeroFF : hexWithoutZeroFF;
if (hexWithoutZeroFF.length() > hexLenMax) {
return hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - hexLenMax);
} else if (hexWithoutZeroFF.length() == hexLenMax) {
return hexWithoutZeroFF;
} else {
return "FF" + hexWithoutZeroFF;
}
}
}
public static String floatToHex(Float f) {
return floatToHex(f, true);
}
public static String floatToHex(Float f, boolean bigEndian) {
// Convert the float to its raw integer bits representation
int bits = Float.floatToRawIntBits(f);
// Format the integer bits as a hexadecimal string
String result = String.format("0x%08X", bits);
return bigEndian ? result : reverseHexStringByOrder(result);
}
public static String doubleToHex(Double d) {
return doubleToHex(d, true);
}
public static String doubleToHex(Double d, boolean bigEndian) {
long bits = Double.doubleToRawLongBits(d);
// Format the integer bits as a hexadecimal string
String result = String.format("0x%16X", bits);
return bigEndian ? result : reverseHexStringByOrder(result);
}
public static String base64ToHex(String base64) { public static String base64ToHex(String base64) {
return bytesToHex(Base64.getDecoder().decode(base64)); return bytesToHex(Base64.getDecoder().decode(base64));
} }
@ -520,8 +755,8 @@ public class TbUtils {
if (offset > data.length) { if (offset > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!"); throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
} }
if (length > 8) { if (length > BYTES_LEN_LONG_MAX) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!"); throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!");
} }
if (offset + length > data.length) { if (offset + length > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!"); throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
@ -567,7 +802,7 @@ public class TbUtils {
} }
public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) { public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, 8, bigEndian); byte[] bytesToNumber = prepareBytesToNumber(data, offset, BYTES_LEN_LONG_MAX, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getDouble(); return ByteBuffer.wrap(bytesToNumber).getDouble();
} }
@ -647,6 +882,15 @@ public class TbUtils {
return URLDecoder.decode(uri, StandardCharsets.UTF_8); return URLDecoder.decode(uri, StandardCharsets.UTF_8);
} }
public static void raiseError(String message) {
raiseError(message, null);
}
public static void raiseError(String message, Object value) {
String msg = value == null ? message : message + " for value " + value;
throw new RuntimeException(msg);
}
private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) { private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) {
if (json instanceof Map.Entry) { if (json instanceof Map.Entry) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) json; Map.Entry<?, ?> entry = (Map.Entry<?, ?>) json;
@ -687,42 +931,104 @@ public class TbUtils {
} }
} }
private static boolean isHexadecimal(String value) {
return value != null && (value.contains("0x") || value.contains("0X"));
}
private static String prepareNumberString(String value) { private static String prepareNumberString(String value) {
if (value != null) { if (value != null) {
value = value.trim(); value = value.trim();
if (isHexadecimal(value)) { value = value.replace("0x", "");
value = value.replace("0x", ""); value = value.replace("0X", "");
value = value.replace("0X", "");
}
value = value.replace(",", "."); value = value.replace(",", ".");
} }
return value; return value;
} }
private static boolean isValidRadix(String value, int radix) { private static int isValidStringAndRadix(String valueP, int radix, String value) {
for (int i = 0; i < value.length(); i++) { int radixValue;
if (i == 0 && value.charAt(i) == '-') { if (radix == 0) {
if (value.length() == 1) radixValue = getRadix10_16(valueP);
throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\"!"); } else if (radix >= 25 && radix <= MAX_RADIX) {
else return radix;
continue; } else {
radixValue = switch (radix) {
case MIN_RADIX -> isBinary(valueP);
case OCTAL_RADIX -> isOctal(valueP);
case DEC_RADIX -> isDecimal(valueP);
case HEX_RADIX -> isHexadecimal(valueP);
default -> throw new IllegalArgumentException("Invalid radix: [" + radix + "]");
};
}
if (radixValue > 0) {
if (value.startsWith("0x")) radixValue = HEX_RADIX;
if (radixValue == HEX_RADIX) {
valueP = value.startsWith("-") ? value.substring(1) : value;
if (valueP.length() % 2 > 0) {
throw new NumberFormatException("The hexadecimal value: \"" + value + "\" must be of even length, or if the decimal value must be a number!");
}
}
return radixValue;
} else {
if (radix > 0) {
throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\", must be [" + radixValue + "] !");
} else {
throw new NumberFormatException("Invalid \"" + value + "\". It is not numeric or hexadecimal format!");
} }
if (Character.digit(value.charAt(i), radix) < 0)
throw new NumberFormatException("Failed radix: [" + radix + "] for value: \"" + value + "\"!");
} }
return true;
} }
private static byte isValidIntegerToByte (Integer val) { public static int isBinary(String str) {
if (val > 255 || val.intValue() < -128) { if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("[01]+") ? MIN_RADIX : -1;
}
public static int isOctal(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("[0-7]+") ? OCTAL_RADIX : -1;
}
public static int isDecimal(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("-?\\d+(\\.\\d+)?") ? DEC_RADIX : -1;
}
public static int isHexadecimal(String str) {
if (str == null || str.isEmpty()) {
return -1;
}
return str.matches("^-?(0[xX])?[0-9a-fA-F]+$") ? HEX_RADIX : -1;
}
private static byte isValidIntegerToByte(Integer val) {
if (val > 255 || val < -128) {
throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " +
"Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!"); "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!");
} else { } else {
return val.byteValue(); return val.byteValue();
} }
} }
private static String reverseHexStringByOrder(String value) {
if (value.startsWith("-")) {
throw new IllegalArgumentException("The hexadecimal string must be without a negative sign.");
}
boolean isHexPref = value.startsWith("0x");
String hex = isHexPref ? value.substring(2) : value;
if (hex.length() % 2 > 0) {
throw new IllegalArgumentException("The hexadecimal string must be even-length.");
}
// Split the hex string into bytes (2 characters each)
StringBuilder reversedHex = new StringBuilder(BYTES_LEN_LONG_MAX);
for (int i = hex.length() - 2; i >= 0; i -= 2) {
reversedHex.append(hex, i, i + 2);
}
String result = reversedHex.toString();
return isHexPref ? "0x" + result : result;
}
} }

302
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -15,12 +15,14 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;
import org.mvel2.ParserContext; import org.mvel2.ParserContext;
import org.mvel2.SandboxedParserConfiguration; import org.mvel2.SandboxedParserConfiguration;
@ -31,28 +33,28 @@ import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import static java.lang.Character.MAX_RADIX;
import static java.lang.Character.MIN_RADIX;
@Slf4j @Slf4j
public class TbUtilsTest { public class TbUtilsTest {
private ExecutionContext ctx; private ExecutionContext ctx;
private final String intValHex = "41EA62CC";
private final float floatVal = 29.29824f; private final float floatVal = 29.29824f;
private final String floatValStr = "29.29824";
private final String floatValHexRev = "CC62EA41";
private final float floatValRev = -5.948442E7f; private final float floatValRev = -5.948442E7f;
private final long longVal = 0x409B04B10CB295EAL; private final long longVal = 0x409B04B10CB295EAL;
private final String longValHex = "409B04B10CB295EA"; private final String longValHex = "409B04B10CB295EA";
private final long longValRev = 0xEA95B20CB1049B40L;
private final String longValHexRev = "EA95B20CB1049B40"; private final String longValHexRev = "EA95B20CB1049B40";
private final String doubleValStr = "1729.1729";
private final double doubleVal = 1729.1729; private final double doubleVal = 1729.1729;
private final double doubleValRev = -2.7208640774822924E205; private final double doubleValRev = -2.7208640774822924E205;
@ -86,10 +88,9 @@ public class TbUtilsTest {
Assertions.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false)); Assertions.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false));
Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true)); Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true));
Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("CCBBAA", false)); Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("CCBBAA", false));
Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("AABBCCDD", true)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseHexToInt("AABBCCDD", true));
Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("DDCCBBAA", false)); Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("11BBCC22", true));
Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("DDCCBBAA", true)); Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("22CCBB11", false));
Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("AABBCCDD", false));
} }
@Test @Test
@ -208,15 +209,28 @@ public class TbUtilsTest {
Assertions.assertNull(TbUtils.parseInt("")); Assertions.assertNull(TbUtils.parseInt(""));
Assertions.assertNull(TbUtils.parseInt(" ")); Assertions.assertNull(TbUtils.parseInt(" "));
Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0")); Assertions.assertEquals((Integer) 0, TbUtils.parseInt("0"));
Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("-0")); Assertions.assertEquals((Integer) 0, TbUtils.parseInt("-0"));
Assertions.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473")); Assertions.assertEquals(java.util.Optional.of(473).get(), TbUtils.parseInt("473"));
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF")); Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-0xFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FF")); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("-0xFF123"));
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF"));
Assertions.assertEquals(java.util.Optional.of(255).get(), TbUtils.parseInt("FF"));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("FFF"));
Assertions.assertEquals(java.util.Optional.of(-2578).get(), TbUtils.parseInt("-0A12"));
Assertions.assertEquals(java.util.Optional.of(-2578).get(), TbUtils.parseHexToInt("-0A12"));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseHexToInt("A12", false));
Assertions.assertEquals(java.util.Optional.of(-14866).get(), TbUtils.parseBigEndianHexToInt("-3A12"));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseLittleEndianHexToInt("-A12"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("0xFG")); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("0xFG"));
Assertions.assertEquals(java.util.Optional.of(102).get(), TbUtils.parseInt("1100110", 2)); Assertions.assertEquals(java.util.Optional.of(102).get(), TbUtils.parseInt("1100110", 2));
Assertions.assertEquals(java.util.Optional.of(-102).get(), TbUtils.parseInt("1111111111111111111111111111111111111111111111111111111110011010", 2));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("1100210", 2)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("1100210", 2));
Assertions.assertEquals(java.util.Optional.of(13158).get(), TbUtils.parseInt("11001101100110", 2));
Assertions.assertEquals(java.util.Optional.of(-13158).get(), TbUtils.parseInt("1111111111111111111111111111111111111111111111111100110010011010", 2));
Assertions.assertEquals(java.util.Optional.of(63).get(), TbUtils.parseInt("77", 8)); Assertions.assertEquals(java.util.Optional.of(63).get(), TbUtils.parseInt("77", 8));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("18", 8)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("18", 8));
@ -224,19 +238,32 @@ public class TbUtilsTest {
Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF", 16)); Assertions.assertEquals(java.util.Optional.of(-255).get(), TbUtils.parseInt("-FF", 16));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FG", 16)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("FG", 16));
Assertions.assertEquals(java.util.Optional.of(Integer.MAX_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MAX_VALUE), 10)); Assertions.assertEquals(java.util.Optional.of(Integer.MAX_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MAX_VALUE), 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.valueOf(1)).toString(10), 10));
Assertions.assertEquals(java.util.Optional.of(Integer.MIN_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MIN_VALUE), 10)); Assertions.assertEquals(java.util.Optional.of(Integer.MIN_VALUE).get(), TbUtils.parseInt(Integer.toString(Integer.MIN_VALUE), 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.valueOf(1)).toString(10), 10));
Assertions.assertEquals(java.util.Optional.of(506070563).get(), TbUtils.parseInt("KonaIn", 30)); Assertions.assertEquals(java.util.Optional.of(506070563).get(), TbUtils.parseInt("KonaIn", 30));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("KonaIn", 10)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("KonaIn", 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt(".456", 10));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseInt("4562.", 10));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", MAX_RADIX + 1));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", MIN_RADIX - 1));
Assertions.assertThrows(IllegalArgumentException.class, () -> TbUtils.parseInt("KonaIn", 12));
} }
@Test @Test
public void parseFloat() { public void parseFloat() {
String floatValStr = "29.29824";
Assertions.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr)); Assertions.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr));
String floatValHex = "41EA62CC";
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(floatValHex)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(floatValHex, false)));
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(floatValHex)));
String floatValHexRev = "CC62EA41";
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev)));
} }
@Test @Test
@ -246,21 +273,13 @@ public class TbUtilsTest {
Assertions.assertEquals(0, Float.compare(29.298f, actualF)); Assertions.assertEquals(0, Float.compare(29.298f, actualF));
} }
@Test
public void parseHexToFloat() {
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(intValHex)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseHexToFloat(intValHex, false)));
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBigEndianHexToFloat(intValHex)));
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseLittleEndianHexToFloat(floatValHexRev)));
}
@Test @Test
public void arseBytesToFloat() { public void arseBytesToFloat() {
byte[] floatValByte = {65, -22, 98, -52}; byte[] floatValByte = {65, -22, 98, -52};
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0))); Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false))); Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false)));
List <Byte> floatVaList = Bytes.asList(floatValByte); List<Byte> floatVaList = Bytes.asList(floatValByte);
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0))); Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false))); Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false)));
} }
@ -271,14 +290,15 @@ public class TbUtilsTest {
Assertions.assertNull(TbUtils.parseLong("")); Assertions.assertNull(TbUtils.parseLong(""));
Assertions.assertNull(TbUtils.parseLong(" ")); Assertions.assertNull(TbUtils.parseLong(" "));
Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0")); Assertions.assertEquals((Long) 0L, TbUtils.parseLong("0"));
Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("-0")); Assertions.assertEquals((Long) 0L, TbUtils.parseLong("-0"));
Assertions.assertEquals(java.util.Optional.of(473L).get(), TbUtils.parseLong("473")); Assertions.assertEquals(java.util.Optional.of(473L).get(), TbUtils.parseLong("473"));
Assertions.assertEquals(java.util.Optional.of(-65535L).get(), TbUtils.parseLong("-0xFFFF")); Assertions.assertEquals(java.util.Optional.of(-65535L).get(), TbUtils.parseLong("-0xFFFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("FFFFFFFF")); Assertions.assertEquals(java.util.Optional.of(4294967295L).get(), TbUtils.parseLong("FFFFFFFF"));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("0xFGFFFFFF")); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("0xFGFFFFFF"));
Assertions.assertEquals(java.util.Optional.of(13158L).get(), TbUtils.parseLong("11001101100110", 2)); Assertions.assertEquals(java.util.Optional.of(13158L).get(), TbUtils.parseLong("11001101100110", 2));
Assertions.assertEquals(java.util.Optional.of(-13158L).get(), TbUtils.parseLong("1111111111111111111111111111111111111111111111111100110010011010", 2));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("11001101100210", 2)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("11001101100210", 2));
Assertions.assertEquals(java.util.Optional.of(9223372036854775807L).get(), TbUtils.parseLong("777777777777777777777", 8)); Assertions.assertEquals(java.util.Optional.of(9223372036854775807L).get(), TbUtils.parseLong("777777777777777777777", 8));
@ -295,10 +315,7 @@ public class TbUtilsTest {
Assertions.assertEquals(java.util.Optional.of(218840926543L).get(), TbUtils.parseLong("KonaLong", 27)); Assertions.assertEquals(java.util.Optional.of(218840926543L).get(), TbUtils.parseLong("KonaLong", 27));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10)); Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10));
}
@Test
public void parseHexToLong() {
Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHex)); Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHex));
Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false)); Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false));
Assertions.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex)); Assertions.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex));
@ -312,14 +329,20 @@ public class TbUtilsTest {
Bytes.reverse(longValByte); Bytes.reverse(longValByte);
Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8, false)); Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longValByte, 0, 8, false));
List <Byte> longVaList = Bytes.asList(longValByte); List<Byte> longVaList = Bytes.asList(longValByte);
Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longVaList, 0, 8, false)); Assertions.assertEquals(longVal, TbUtils.parseBytesToLong(longVaList, 0, 8, false));
long longValRev = 0xEA95B20CB1049B40L;
Assertions.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8)); Assertions.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8));
} }
@Test @Test
public void parsDouble() { public void parsDouble() {
String doubleValStr = "1729.1729";
Assertions.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr)); Assertions.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev)));
} }
@Test @Test
@ -329,21 +352,13 @@ public class TbUtilsTest {
Assertions.assertEquals(0, Double.compare(1729.173, actualD)); Assertions.assertEquals(0, Double.compare(1729.173, actualD));
} }
@Test
public void parseHexToDouble() {
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex)));
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseLittleEndianHexToDouble(longValHexRev)));
}
@Test @Test
public void parseBytesToDouble() { public void parseBytesToDouble() {
byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22}; byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22};
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0))); Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false)));
List <Byte> doubleVaList = Bytes.asList(doubleValByte); List<Byte> doubleVaList = Bytes.asList(doubleValByte);
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0))); Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
} }
@ -354,16 +369,17 @@ public class TbUtilsTest {
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx); ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world"); expectedJson.put("hello", "world");
List<Byte> expectedBytes = TbUtils.stringToBytes(ctx, expectedStr); List<Byte> expectedBytes = TbUtils.stringToBytes(ctx, expectedStr);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes); Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assertions.assertEquals(expectedJson,actualJson); Assertions.assertEquals(expectedJson, actualJson);
} }
@Test @Test
public void parseStringDecodeToJson() throws IOException { public void parseStringDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}"; String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx); ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world"); expectedJson.put("hello", "world");
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr); Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assertions.assertEquals(expectedJson,actualJson); Assertions.assertEquals(expectedJson, actualJson);
} }
@Test @Test
@ -386,8 +402,8 @@ public class TbUtilsTest {
@Test @Test
public void bytesFromList() { public void bytesFromList() {
byte[] arrayBytes = {(byte)0x00, (byte)0x08, (byte)0x10, (byte)0x1C, (byte)0xFF, (byte)0xFC, (byte)0xAD, (byte)0x88, (byte)0x75, (byte)0x74, (byte)0x8A, (byte)0x82}; byte[] arrayBytes = {(byte) 0x00, (byte) 0x08, (byte) 0x10, (byte) 0x1C, (byte) 0xFF, (byte) 0xFC, (byte) 0xAD, (byte) 0x88, (byte) 0x75, (byte) 0x74, (byte) 0x8A, (byte) 0x82};
Object[] arrayMix = { "0x00", 8, "16", "0x1C", 255, (byte)0xFC, 173, 136, 117, 116, -118, "-126"}; Object[] arrayMix = {"0x00", 8, "16", "0x1C", 255, (byte) 0xFC, 173, 136, 117, 116, -118, "-126"};
String expected = new String(arrayBytes); String expected = new String(arrayBytes);
ArrayList<Byte> listBytes = new ArrayList<>(arrayBytes.length); ArrayList<Byte> listBytes = new ArrayList<>(arrayBytes.length);
@ -397,12 +413,36 @@ public class TbUtilsTest {
Assertions.assertEquals(expected, TbUtils.bytesToString(listBytes)); Assertions.assertEquals(expected, TbUtils.bytesToString(listBytes));
ArrayList<Object> listMix = new ArrayList<>(arrayMix.length); ArrayList<Object> listMix = new ArrayList<>(arrayMix.length);
for (Object element : arrayMix) { Collections.addAll(listMix, arrayMix);
listMix.add(element);
}
Assertions.assertEquals(expected, TbUtils.bytesToString(listMix)); Assertions.assertEquals(expected, TbUtils.bytesToString(listMix));
} }
@Test
public void bytesFromList_SpecSymbol() {
List<String> listHex = new ArrayList<>(Arrays.asList("1D", "0x1D", "1F", "0x1F", "0x20", "0x20"));
byte[] expectedBytes = new byte[]{29, 29, 31, 31, 32, 32};
String actualStr = TbUtils.bytesToString(listHex);
byte[] actualBytes = actualStr.getBytes();
Assertions.assertArrayEquals(expectedBytes, actualBytes);
Assertions.assertTrue(actualStr.isBlank());
listHex = new ArrayList<>(Arrays.asList("0x21", "0x21"));
expectedBytes = new byte[]{33, 33};
actualStr = TbUtils.bytesToString(listHex);
actualBytes = actualStr.getBytes();
Assertions.assertArrayEquals(expectedBytes, actualBytes);
Assertions.assertFalse(actualStr.isBlank());
Assertions.assertEquals("!!", actualStr);
listHex = new ArrayList<>(Arrays.asList("21", "0x21"));
expectedBytes = new byte[]{21, 33};
actualStr = TbUtils.bytesToString(listHex);
actualBytes = actualStr.getBytes();
Assertions.assertArrayEquals(expectedBytes, actualBytes);
Assertions.assertFalse(actualStr.isBlank());
Assertions.assertEquals("!", actualStr.substring(1));
Assertions.assertEquals('\u0015', actualStr.charAt(0));
Assertions.assertEquals(21, actualStr.charAt(0));
}
@Test @Test
public void bytesFromList_Error() { public void bytesFromList_Error() {
List<String> listHex = new ArrayList<>(); List<String> listHex = new ArrayList<>();
@ -411,14 +451,7 @@ public class TbUtilsTest {
TbUtils.bytesToString(listHex); TbUtils.bytesToString(listHex);
Assertions.fail("Should throw NumberFormatException"); Assertions.fail("Should throw NumberFormatException");
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Assertions.assertTrue(e.getMessage().contains("Failed radix: [16] for value: \"FG\"!")); Assertions.assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!"));
}
listHex.add(0, "1F");
try {
TbUtils.bytesToString(listHex);
Assertions.fail("Should throw NumberFormatException");
} catch (NumberFormatException e) {
Assertions.assertTrue(e.getMessage().contains("Failed radix: [10] for value: \"1F\"!"));
} }
List<String> listIntString = new ArrayList<>(); List<String> listIntString = new ArrayList<>();
@ -482,6 +515,161 @@ public class TbUtilsTest {
String uriDecodeActual = TbUtils.decodeURI(uriEncodeActual); String uriDecodeActual = TbUtils.decodeURI(uriEncodeActual);
Assertions.assertEquals(uriOriginal, uriDecodeActual); Assertions.assertEquals(uriOriginal, uriDecodeActual);
} }
@Test
public void intToHex_Test() {
Assertions.assertEquals("FFF5EE", TbUtils.intToHex(-2578));
Assertions.assertEquals("0xFFD8FFA6", TbUtils.intToHex(0xFFD8FFA6, true, true));
Assertions.assertEquals("0xA6FFD8FF", TbUtils.intToHex(0xFFD8FFA6, false, true));
Assertions.assertEquals("0x7FFFFFFF", TbUtils.intToHex(Integer.MAX_VALUE, true, true));
Assertions.assertEquals("0x80000000", TbUtils.intToHex(Integer.MIN_VALUE, true, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true));
Assertions.assertEquals("0xABCD", TbUtils.intToHex(0xABCD, true, true));
Assertions.assertEquals("0xABCDEF", TbUtils.intToHex(0xABCDEF, true, true));
Assertions.assertEquals("0xCDAB", TbUtils.intToHex(0xABCDEF, false, true, 4));
Assertions.assertEquals("0xAB", TbUtils.intToHex(171, true, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, false, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true, 2));
Assertions.assertEquals("AB", TbUtils.intToHex(0xAB, false, false, 2));
Assertions.assertEquals("AB", TbUtils.intToHex(171, true, false));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, true, true));
Assertions.assertEquals("0xAB", TbUtils.intToHex(0xAB, false, true));
Assertions.assertEquals("AB", TbUtils.intToHex(0xAB, false, false));
Assertions.assertEquals("0xABCD", TbUtils.intToHex(0xABCD, true, true));
Assertions.assertEquals("0xCDAB", TbUtils.intToHex(0xABCD, false, true));
Assertions.assertEquals("0xCD", TbUtils.intToHex(0xABCD, true, true, 2));
Assertions.assertEquals("AB", TbUtils.intToHex(0xABCD, false, false, 2));
}
@Test
public void longToHex_Test() {
Assertions.assertEquals("0x7FFFFFFFFFFFFFFF", TbUtils.longToHex(Long.MAX_VALUE, true, true));
Assertions.assertEquals("0x8000000000000000", TbUtils.longToHex(Long.MIN_VALUE, true, true));
Assertions.assertEquals("0xFFD8FFA6FFD8FFA6", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, true, true));
Assertions.assertEquals("0xA6FFD8FFA6FFCEFF", TbUtils.longToHex(0xFFCEFFA6FFD8FFA6L, false, true));
Assertions.assertEquals("0xAB", TbUtils.longToHex(0xABL, true, true));
Assertions.assertEquals("0xABCD", TbUtils.longToHex(0xABCDL, true, true));
Assertions.assertEquals("0xABCDEF", TbUtils.longToHex(0xABCDEFL, true, true));
Assertions.assertEquals("0xABEFCDAB", TbUtils.longToHex(0xABCDEFABCDEFL, false, true, 8));
Assertions.assertEquals("0xAB", TbUtils.longToHex(0xABL, true, true, 2));
Assertions.assertEquals("AB", TbUtils.longToHex(0xABL, false, false, 2));
Assertions.assertEquals("0xFFA6", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, true, true, 4));
Assertions.assertEquals("D8FF", TbUtils.longToHex(0xFFD8FFA6FFD8FFA6L, false, false, 4));
}
@Test
public void numberToString_Test() {
Assertions.assertEquals("11001101100110", TbUtils.intLongToString(13158L, 2));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, 2));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, 2));
Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8));
Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8));
Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE));
Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE));
Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16));
Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16));
Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true));
Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true));
Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16));
Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX));
}
@Test
public void intToHexWithPrintUnsignedBytes_Test() {
Integer value = -40;
String intToHexLe = TbUtils.intToHex(value, false, true);
String intToHexBe = TbUtils.intToHex(value, true, true);
List<Byte> hexTopByteLe = TbUtils.hexToBytes(ctx, intToHexLe);
List<Byte> hexTopByteBe = TbUtils.hexToBytes(ctx, intToHexBe);
byte[] arrayBytes = {-40, -1};
List<Byte> expectedHexTopByteLe = Bytes.asList(arrayBytes);
List<Byte> expectedHexTopByteBe = Lists.reverse(expectedHexTopByteLe);
Assertions.assertEquals(expectedHexTopByteLe, hexTopByteLe);
Assertions.assertEquals(expectedHexTopByteBe, hexTopByteBe);
List<Integer> actualLe = TbUtils.printUnsignedBytes(ctx, hexTopByteLe);
List<Integer> actualBe = TbUtils.printUnsignedBytes(ctx, hexTopByteBe);
List<Integer> expectedLe = Ints.asList(216, 255);
List<Integer> expectedBe = Lists.reverse(expectedLe);
Assertions.assertEquals(expectedLe, actualLe);
Assertions.assertEquals(expectedBe, actualBe);
}
@Test
public void floatToHex_Test() {
Float value = 20.89f;
String expectedHex = "0x41A71EB8";
String valueHexRev = "0xB81EA741";
String actual = TbUtils.floatToHex(value);
Assertions.assertEquals(expectedHex, actual);
Float valueActual = TbUtils.parseHexToFloat(actual);
Assertions.assertEquals(value, valueActual);
valueActual = TbUtils.parseHexToFloat(valueHexRev, false);
Assertions.assertEquals(value, valueActual);
}
@Test
public void doubleToHex_Test() {
String expectedHex = "0x409B04B10CB295EA";
String actual = TbUtils.doubleToHex(doubleVal);
Assertions.assertEquals(expectedHex, actual);
Double valueActual = TbUtils.parseHexToDouble(actual);
Assertions.assertEquals(doubleVal, valueActual);
actual = TbUtils.doubleToHex(doubleVal, false);
String expectedHexRev = "0xEA95B20CB1049B40";
Assertions.assertEquals(expectedHexRev, actual);
}
@Test
public void raiseError_Test() {
String message = "frequency_weighting_type must be 0, 1 or 2.";
Object value = 4;
try {
TbUtils.raiseError(message, value);
Assertions.fail("Should throw NumberFormatException");
} catch (RuntimeException e) {
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4"));
}
try {
TbUtils.raiseError(message);
Assertions.fail("Should throw NumberFormatException");
} catch (RuntimeException e) {
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2."));
}
}
@Test
public void isBinary_Test() {
Assertions.assertEquals(2, TbUtils.isBinary("1100110"));
Assertions.assertEquals(-1, TbUtils.isBinary("2100110"));
}
@Test
public void isOctal_Test() {
Assertions.assertEquals(8, TbUtils.isOctal("4567734"));
Assertions.assertEquals(-1, TbUtils.isOctal("8100110"));
}
@Test
public void isDecimal_Test() {
Assertions.assertEquals(10, TbUtils.isDecimal("4567039"));
Assertions.assertEquals(-1, TbUtils.isDecimal("C100110"));
}
@Test
public void isHexadecimal_Test() {
Assertions.assertEquals(16, TbUtils.isHexadecimal("F5D7039"));
Assertions.assertEquals(-1, TbUtils.isHexadecimal("K100110"));
}
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length); List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) { for (Byte b : data) {

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -948,7 +948,7 @@ public class DefaultTransportService extends TransportActivityManager implements
String resourceId = msg.getResourceKey(); String resourceId = msg.getResourceKey();
transportResourceCache.evict(tenantId, resourceType, resourceId); transportResourceCache.evict(tenantId, resourceType, resourceId);
sessions.forEach((id, mdRez) -> { sessions.forEach((id, mdRez) -> {
log.warn("ResourceDelete - [{}] [{}]", id, mdRez); log.trace("ResourceDelete - [{}] [{}]", id, mdRez);
transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg)); transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg));
}); });
} else if (toSessionMsg.getQueueUpdateMsgsCount() > 0) { } else if (toSessionMsg.getQueueUpdateMsgsCount() > 0) {

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

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.dashboard;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -57,6 +56,7 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
@ -235,13 +235,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
publishEvictEvent(new DashboardTitleEvictEvent(dashboardId)); publishEvictEvent(new DashboardTitleEvictEvent(dashboardId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD); countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(dashboardId).build()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(dashboardId).build());
} catch (Exception t) { } catch (Exception e) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); checkConstraintViolation(e, Map.of(
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) { "fk_default_dashboard_device_profile", "The dashboard is referenced by a device profile",
throw new DataValidationException("The dashboard referenced by the device profiles cannot be deleted!"); "fk_default_dashboard_asset_profile", "The dashboard is referenced by an asset profile"
} else { ));
throw t; throw e;
}
} }
} }

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

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
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.SortOrder;
@ -34,6 +35,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
@Service @Service
@Slf4j @Slf4j
@ -72,10 +74,10 @@ public class DefaultNotificationService implements NotificationService, EntityDa
} }
@Override @Override
public PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit) { public PageData<Notification> findLatestUnreadNotificationsByRecipientIdAndNotificationTypes(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, Set<NotificationType> types, int limit) {
SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC); SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC);
PageLink pageLink = new PageLink(limit, 0, null, sortOrder); PageLink pageLink = new PageLink(limit, 0, null, sortOrder);
return findNotificationsByRecipientIdAndReadStatus(tenantId, deliveryMethod, recipientId, true, pageLink); return notificationDao.findUnreadByDeliveryMethodAndRecipientIdAndNotificationTypesAndPageLink(tenantId, deliveryMethod, recipientId, types, pageLink);
} }
@Override @Override

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

@ -22,14 +22,19 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
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.Dao; import org.thingsboard.server.dao.Dao;
import java.util.Set;
public interface NotificationDao extends Dao<Notification> { public interface NotificationDao extends Dao<Notification> {
PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink); PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink);
PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndNotificationTypesAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, Set<NotificationType> types, PageLink pageLink);
PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink); PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink);
boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status); boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status);

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

@ -342,9 +342,12 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override @Override
public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) { public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) {
UUID tenantUUID = request.getTenantId().getId();
log.debug("[{}] createOrUpdateActiveAlarm [{}] {}", tenantUUID, alarmCreationEnabled, request);
AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation());
return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm( return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm(
request.getTenantId().getId(), tenantUUID,
request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID, request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID,
request.getEdgeAlarmId() != null ? request.getEdgeAlarmId().getId() : UUID.randomUUID(), request.getEdgeAlarmId() != null ? request.getEdgeAlarmId().getId() : UUID.randomUUID(),
System.currentTimeMillis(), System.currentTimeMillis(),
@ -364,10 +367,14 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override @Override
public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) {
UUID tenantUUID = request.getTenantId().getId();
UUID alarmUUID = request.getAlarmId().getId();
log.debug("[{}][{}] updateAlarm {}", tenantUUID, alarmUUID, request);
AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation());
return toAlarmApiResult(alarmRepository.updateAlarm( return toAlarmApiResult(alarmRepository.updateAlarm(
request.getTenantId().getId(), tenantUUID,
request.getAlarmId().getId(), alarmUUID,
request.getSeverity().name(), request.getSeverity().name(),
request.getStartTs(), request.getEndTs(), request.getStartTs(), request.getEndTs(),
getDetailsAsString(request.getDetails()), getDetailsAsString(request.getDetails()),
@ -380,11 +387,13 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override @Override
public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) { public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) {
log.debug("[{}][{}] acknowledgeAlarm [{}]", tenantId, id, ackTs);
return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs)); return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs));
} }
@Override @Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) { public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) {
log.debug("[{}][{}] clearAlarm [{}]", tenantId, id, clearTs);
return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, details != null ? getDetailsAsString(details) : null)); return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, details != null ? getDetailsAsString(details) : null));
} }

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

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.notification; package org.thingsboard.server.dao.sql.notification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
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.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
@ -37,6 +39,7 @@ import org.thingsboard.server.dao.sql.JpaPartitionedAbstractDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -58,6 +61,14 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
} }
@Override @Override
public PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndNotificationTypesAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, Set<NotificationType> types, PageLink pageLink) {
if (CollectionUtils.isEmpty(types)) {
return findUnreadByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink);
}
return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientIdAndTypeInAndStatusNot(deliveryMethod,
recipientId.getId(), types, NotificationStatus.READ, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
public PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) { public PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(), return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(),
pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));

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

@ -25,8 +25,10 @@ import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.dao.model.sql.NotificationEntity; import org.thingsboard.server.dao.model.sql.NotificationEntity;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
@ -42,7 +44,20 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId " + @Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod " +
"AND n.recipientId = :recipientId AND n.status <> :status " +
"AND (n.type IN :types) " +
"AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByDeliveryMethodAndRecipientIdAndTypeInAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("recipientId") UUID recipientId,
@Param("types") Set<NotificationType> types,
@Param("status") NotificationStatus status,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod " +
"AND n.recipientId = :recipientId " +
"AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)") "OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod, Page<NotificationEntity> findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,

2
pom.xml

@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version> <zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet--> <protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.63.0</grpc.version> <grpc.version>1.63.0</grpc.version>
<tbel.version>1.2.0</tbel.version> <tbel.version>1.2.1</tbel.version>
<lombok.version>1.18.32</lombok.version> <lombok.version>1.18.32</lombok.version>
<paho.client.version>1.2.5</paho.client.version> <paho.client.version>1.2.5</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version> <paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java

@ -181,8 +181,11 @@ public class CalculateDeltaNode implements TbNode {
private ListenableFuture<ValueWithTs> getLatestFromCacheOrFetchFromDb(TbContext ctx, TbMsg msg) { private ListenableFuture<ValueWithTs> getLatestFromCacheOrFetchFromDb(TbContext ctx, TbMsg msg) {
EntityId originator = msg.getOriginator(); EntityId originator = msg.getOriginator();
ValueWithTs valueWithTs = cache.get(msg.getOriginator()); if (config.isUseCache()) {
return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator); ValueWithTs valueWithTs = cache.get(msg.getOriginator());
return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator);
}
return fetchLatestValueAsync(ctx, originator);
} }
private record ValueWithTs(long ts, double value) { private record ValueWithTs(long ts, double value) {

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

@ -53,6 +53,7 @@ import java.util.concurrent.TimeoutException;
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "mqtt", name = "mqtt",
configClazz = TbMqttNodeConfiguration.class, configClazz = TbMqttNodeConfiguration.class,
version = 1,
clusteringMode = ComponentClusteringMode.USER_PREFERENCE, clusteringMode = ComponentClusteringMode.USER_PREFERENCE,
nodeDescription = "Publish messages to the MQTT broker", nodeDescription = "Publish messages to the MQTT broker",
nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.", nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",

24
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -27,10 +27,10 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec; import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
@ -132,6 +132,7 @@ public class TbHttpClient {
this.webClient = WebClient.builder() this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient)) .clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset")
.build(); .build();
} catch (SSLException e) { } catch (SSLException e) {
throw new TbNodeException(e); throw new TbNodeException(e);
@ -170,7 +171,7 @@ public class TbHttpClient {
BiConsumer<TbMsg, Throwable> onFailure) { BiConsumer<TbMsg, Throwable> onFailure) {
try { try {
if (semaphore != null && !semaphore.tryAcquire(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)) { if (semaphore != null && !semaphore.tryAcquire(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS)) {
ctx.tellFailure(msg, new RuntimeException("Timeout during waiting for reply!")); onFailure.accept(msg, new RuntimeException("Timeout during waiting for reply!"));
return; return;
} }
@ -183,10 +184,10 @@ public class TbHttpClient {
.uri(uri) .uri(uri)
.headers(headers -> prepareHeaders(headers, msg)); .headers(headers -> prepareHeaders(headers, msg));
if (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) || if ((HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) ||
HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method) || HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method)) &&
!config.isIgnoreRequestBody()) { !config.isIgnoreRequestBody()) {
request.body(BodyInserters.fromValue(getData(msg, config.isIgnoreRequestBody(), config.isParseToPlainText()))); request.body(BodyInserters.fromValue(getData(msg, config.isParseToPlainText())));
} }
request request
@ -236,11 +237,9 @@ public class TbHttpClient {
return uri; return uri;
} }
private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) { private Object getData(TbMsg tbMsg, boolean parseToPlainText) {
if (!ignoreBody && parseToPlainText) { String data = tbMsg.getData();
return JacksonUtil.toPlainText(tbMsg.getData()); return parseToPlainText ? JacksonUtil.toPlainText(data) : JacksonUtil.toJsonNode(data);
}
return tbMsg.getData();
} }
private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) { private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
@ -283,10 +282,9 @@ public class TbHttpClient {
private TbMsg processException(TbMsg origMsg, Throwable e) { private TbMsg processException(TbMsg origMsg, Throwable e) {
TbMsgMetaData metaData = origMsg.getMetaData(); TbMsgMetaData metaData = origMsg.getMetaData();
metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
if (e instanceof RestClientResponseException) { if (e instanceof WebClientResponseException restClientResponseException) {
RestClientResponseException restClientResponseException = (RestClientResponseException) e;
metaData.putValue(STATUS, restClientResponseException.getStatusText()); metaData.putValue(STATUS, restClientResponseException.getStatusText());
metaData.putValue(STATUS_CODE, restClientResponseException.getRawStatusCode() + ""); metaData.putValue(STATUS_CODE, restClientResponseException.getStatusCode().value() + "");
metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString());
} }
return TbMsg.transformMsgMetadata(origMsg, metaData); return TbMsg.transformMsgMetadata(origMsg, metaData);

50
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

19
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java

@ -109,7 +109,6 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
public void setUp() throws TbNodeException { public void setUp() throws TbNodeException {
config = new CalculateDeltaNodeConfiguration().defaultConfiguration(); config = new CalculateDeltaNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
} }
@Test @Test
@ -166,8 +165,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
} }
@Test @Test
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() { public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN // GIVEN
node.init(ctxMock, nodeConfiguration);
var msgData = "{\"pulseCounter\": 42}"; var msgData = "{\"pulseCounter\": 42}";
var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData);
@ -181,8 +181,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
} }
@Test @Test
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() { public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN // GIVEN
node.init(ctxMock, nodeConfiguration);
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY);
// WHEN // WHEN
@ -196,8 +197,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
@Test @Test
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() { public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN // GIVEN
node.init(ctxMock, nodeConfiguration);
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
// WHEN // WHEN
@ -451,8 +453,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
} }
@Test @Test
public void givenInvalidStringValue_whenOnMsg_thenException() { public void givenInvalidStringValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN // GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high")));
var msgData = "{\"pulseCounter\":\"123\"}"; var msgData = "{\"pulseCounter\":\"123\"}";
@ -475,8 +478,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
} }
@Test @Test
public void givenBooleanValue_whenOnMsg_thenException() { public void givenBooleanValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN // GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false)));
var msgData = "{\"pulseCounter\":true}"; var msgData = "{\"pulseCounter\":true}";
@ -499,8 +503,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
} }
@Test @Test
public void givenJsonValue_whenOnMsg_thenException() { public void givenJsonValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN // GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}")));
var msgData = "{\"pulseCounter\":{\"isActive\":true}}"; var msgData = "{\"pulseCounter\":{\"isActive\":true}}";

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

@ -168,7 +168,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
try { try {
assertEquals(path, request.getRequestLine().getUri(), "Request path matches"); assertEquals(path, request.getRequestLine().getUri(), "Request path matches");
assertTrue(request.containsHeader("Content-Type"), "Content-Type included"); assertTrue(request.containsHeader("Content-Type"), "Content-Type included");
assertEquals("text/plain;charset=UTF-8", assertEquals("application/json",
request.getFirstHeader("Content-Type").getValue(), "Content-Type value"); request.getFirstHeader("Content-Type").getValue(), "Content-Type value");
assertTrue(request.containsHeader("Content-Length"), "Content-Length included"); assertTrue(request.containsHeader("Content-Length"), "Content-Length included");
assertEquals("2", assertEquals("2",

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

@ -364,6 +364,16 @@ export function mergeDeep<T>(target: T, ...sources: T[]): T {
return _.merge(target, ...sources); return _.merge(target, ...sources);
} }
function ignoreArrayMergeFunc(target: any, sources: any) {
if (_.isArray(target)) {
return sources;
}
}
export function mergeDeepIgnoreArray<T>(target: T, ...sources: T[]): T {
return _.mergeWith(target, ...sources, ignoreArrayMergeFunc);
}
export function guid(): string { export function guid(): string {
function s4(): string { function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000) return Math.floor((1 + Math.random()) * 0x10000)

6
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html

@ -44,7 +44,11 @@
</section> </section>
</ng-container> </ng-container>
<ng-template #emptyNotification> <ng-template #emptyNotification>
<img src="assets/notification-bell.svg" alt="empty notification" style="margin: 20px 24%"> <div class="tb-no-notification-svg-color" style="margin: 20px 24%">
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%">
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use>
</svg>
</div>
<span style="text-align: center; margin-bottom: 12px" translate>notification.no-notifications-yet</span> <span style="text-align: center; margin-bottom: 12px" translate>notification.no-notifications-yet</span>
</ng-template> </ng-template>
<ng-template #loadingNotification> <ng-template #loadingNotification>

20
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.scss

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

2
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts

@ -29,7 +29,7 @@ import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.model
@Component({ @Component({
selector: 'tb-show-notification-popover', selector: 'tb-show-notification-popover',
templateUrl: './show-notification-popover.component.html', templateUrl: './show-notification-popover.component.html',
styleUrls: [] styleUrls: ['show-notification-popover.component.scss']
}) })
export class ShowNotificationPopoverComponent extends PageComponent implements OnDestroy, OnInit { export class ShowNotificationPopoverComponent extends PageComponent implements OnDestroy, OnInit {

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

@ -139,6 +139,9 @@ import {
import { import {
LabelValueCardBasicConfigComponent LabelValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/label-value-card-basic-config.component'; } from '@home/components/widget/config/basic/cards/label-value-card-basic-config.component';
import {
UnreadNotificationBasicConfigComponent
} from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -185,7 +188,8 @@ import {
DigitalSimpleGaugeBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent, MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent, LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent LabelValueCardBasicConfigComponent,
UnreadNotificationBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -234,7 +238,8 @@ import {
DigitalSimpleGaugeBasicConfigComponent, DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent, MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent, LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent LabelValueCardBasicConfigComponent,
UnreadNotificationBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -277,5 +282,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-digital-simple-gauge-basic-config': DigitalSimpleGaugeBasicConfigComponent, 'tb-digital-simple-gauge-basic-config': DigitalSimpleGaugeBasicConfigComponent,
'tb-mobile-app-qr-code-basic-config': MobileAppQrCodeBasicConfigComponent, 'tb-mobile-app-qr-code-basic-config': MobileAppQrCodeBasicConfigComponent,
'tb-label-card-basic-config': LabelCardBasicConfigComponent, 'tb-label-card-basic-config': LabelCardBasicConfigComponent,
'tb-label-value-card-basic-config': LabelValueCardBasicConfigComponent 'tb-label-value-card-basic-config': LabelValueCardBasicConfigComponent,
'tb-unread-notification-basic-config': UnreadNotificationBasicConfigComponent
}; };

128
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html

@ -0,0 +1,128 @@
<!--
Copyright © 2016-2024 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.
-->
<ng-container [formGroup]="unreadNotificationWidgetConfigForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="unreadNotificationWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.notification.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="unreadNotificationWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.max-notification-display' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="1" formControlName="maxNotificationDisplay" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-row no-padding no-border">
<mat-slide-toggle class="mat-slide" formControlName="showCounter">
{{ 'widgets.notification.counter' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.counter-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="counterValueFont"
clearButton
[previewText]="unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value.toString()"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
formControlName="counterValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.notification.counter-color' | translate }}</div>
<tb-color-input asBoxInput
formControlName="counterColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="viewAll">{{ 'widgets.notification.button-view-all' | translate }}</mat-chip-option>
<mat-chip-option value="filter">{{ 'widgets.notification.button-filter' | translate }}</mat-chip-option>
<mat-chip-option value="markAsRead">{{ 'widgets.notification.button-mark-read' | translate }}</mat-chip-option>
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-padding' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

177
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts

@ -0,0 +1,177 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetConfig, } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isUndefined } from '@core/utils';
import { cssSizeToStrSize, resolveCssSize } from '@shared/models/widget-settings.models';
import {
unreadNotificationDefaultSettings,
UnreadNotificationWidgetSettings
} from '@home/components/widget/lib/cards/unread-notification-widget.models';
@Component({
selector: 'tb-unread-notification-basic-config',
templateUrl: './unread-notification-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigComponent {
unreadNotificationWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.unreadNotificationWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const iconSize = resolveCssSize(configData.config.iconSize);
const settings: UnreadNotificationWidgetSettings = {...unreadNotificationDefaultSettings, ...(configData.config.settings || {})};
this.unreadNotificationWidgetConfigForm = this.fb.group({
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
maxNotificationDisplay: [settings.maxNotificationDisplay, [Validators.required, Validators.min(1)]],
showCounter: [settings.showCounter, []],
counterValueFont: [settings.counterValueFont, []],
counterValueColor: [settings.counterValueColor, []],
counterColor: [settings.counterColor, []],
background: [settings.background, []],
padding: [settings.padding, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected validatorTriggers(): string[] {
return ['showCounter', 'showTitle', 'showIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showCounter: boolean = this.unreadNotificationWidgetConfigForm.get('showCounter').value;
const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.unreadNotificationWidgetConfigForm.get('showIcon').value;
if (showTitle) {
this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent});
}
if (showIcon) {
this.unreadNotificationWidgetConfigForm.get('iconSize').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('icon').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('iconSize').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('icon').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconColor').disable({emitEvent});
}
if (showCounter) {
this.unreadNotificationWidgetConfigForm.get('counterValueFont').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterValueColor').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('counterValueFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent});
}
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.maxNotificationDisplay = config.maxNotificationDisplay;
this.widgetConfig.config.settings.showCounter = config.showCounter;
this.widgetConfig.config.settings.counterValueFont = config.counterValueFont;
this.widgetConfig.config.settings.counterValueColor = config.counterValueColor;
this.widgetConfig.config.settings.counterColor = config.counterColor;
this.widgetConfig.config.settings.background = config.background;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
return this.widgetConfig;
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.settings?.enableViewAll) || config.settings?.enableViewAll) {
buttons.push('viewAll');
}
if (isUndefined(config.settings?.enableFilter) || config.settings?.enableFilter) {
buttons.push('filter');
}
if (isUndefined(config.settings?.enableMarkAsRead) || config.settings?.enableMarkAsRead) {
buttons.push('markAsRead');
}
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.settings.enableViewAll = buttons.includes('viewAll');
config.settings.enableFilter = buttons.includes('filter');
config.settings.enableMarkAsRead = buttons.includes('markAsRead');
config.enableFullscreen = buttons.includes('fullscreen');
}
}

4
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts

@ -33,7 +33,7 @@ import {
getTimewindowConfig, getTimewindowConfig,
setTimewindowConfig setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component'; } from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isUndefined, mergeDeep } from '@core/utils'; import { formatValue, isUndefined, mergeDeepIgnoreArray } from '@core/utils';
import { import {
cssSizeToStrSize, cssSizeToStrSize,
DateFormatProcessor, DateFormatProcessor,
@ -117,7 +117,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
} }
protected onConfigSet(configData: WidgetConfigComponentData) { protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: RangeChartWidgetSettings = mergeDeep<RangeChartWidgetSettings>({} as RangeChartWidgetSettings, const settings: RangeChartWidgetSettings = mergeDeepIgnoreArray<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings); rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings);
const iconSize = resolveCssSize(configData.config.iconSize); const iconSize = resolveCssSize(configData.config.iconSize);
this.rangeChartWidgetConfigForm = this.fb.group({ this.rangeChartWidgetConfigForm = this.fb.group({

2
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts

@ -46,12 +46,12 @@ import {
TimeSeriesChartWidgetSettings TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; } from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { import {
TimeSeriesChartTooltipTrigger,
TimeSeriesChartKeySettings, TimeSeriesChartKeySettings,
TimeSeriesChartType, TimeSeriesChartType,
TimeSeriesChartYAxes, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { TimeSeriesChartTooltipTrigger } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
@Component({ @Component({
selector: 'tb-time-series-chart-basic-config', selector: 'tb-time-series-chart-basic-config',

77
ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.html

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form fxLayout="column" fxFlex class="mat-content mat-padding" (ngSubmit)="update()">
<div class="tb-form-panel no-padding">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" ngClass.xs="filters-title-mobile" translate>widgets.notification.notification-types</div>
<mat-form-field floatLabel="auto" appearance="outline" subscriptSizing="dynamic" class="flex fb-chips">
<mat-chip-grid #chipList>
<mat-chip-row
*ngFor="let type of selectedNotificationTypes"
removable
(removed)="remove(type)">
{{ notificationTypesTranslateMap.get(type).name | translate }}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text"
placeholder="{{ ( selectedNotificationTypes.length ? 'widgets.notification.notification-type' : 'widgets.notification.any-type' ) | translate }}"
style="max-width: 200px;"
[formControl]="searchControlName"
#notificationTypeInput
(focusin)="onFocus()"
matAutocompleteOrigin
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
[matAutocomplete]="NotificationTypeAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="chipAdd($event)">
</mat-chip-grid>
<mat-autocomplete #NotificationTypeAutocomplete="matAutocomplete"
class="tb-autocomplete"
(optionSelected)="selected($event)"
[displayWith]="displayTypeFn">
<mat-option *ngFor="let type of filteredNotificationTypesList | async" [value]="type">
<span [innerHTML]="notificationTypesTranslateMap.get(type).name | translate | highlight:searchText"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="reset()"
color="primary">
{{ 'action.reset' | translate }}
</button>
<span fxFlex></span>
<button type="button"
(click)="cancel()"
mat-button>
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary">
{{ 'action.update' | translate }}
</button>
</div>
</form>

25
ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.scss

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
display: flex;
width: 100%;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}

140
ui-ngx/src/app/modules/home/components/widget/lib/cards/notification-type-filter-panel.component.ts

@ -0,0 +1,140 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, Inject, InjectionToken, OnInit, ViewChild } from '@angular/core';
import { NotificationTemplateTypeTranslateMap, NotificationType } from '@shared/models/notification.models';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { Observable } from 'rxjs';
import { FormControl } from '@angular/forms';
import { debounceTime, map } from 'rxjs/operators';
import { OverlayRef } from '@angular/cdk/overlay';
export const NOTIFICATION_TYPE_FILTER_PANEL_DATA = new InjectionToken<any>('NotificationTypeFilterPanelData');
export interface NotificationTypeFilterPanelData {
notificationTypes: Array<NotificationType>;
notificationTypesUpdated: (notificationTypes: Array<NotificationType>) => void;
}
@Component({
selector: 'tb-notification-type-filter-panel',
templateUrl: './notification-type-filter-panel.component.html',
styleUrls: ['notification-type-filter-panel.component.scss']
})
export class NotificationTypeFilterPanelComponent implements OnInit{
@ViewChild('searchInput') searchInputField: ElementRef;
searchText = '';
searchControlName = new FormControl('');
filteredNotificationTypesList: Observable<Array<NotificationType>>;
selectedNotificationTypes: Array<NotificationType> = [];
notificationTypesTranslateMap = NotificationTemplateTypeTranslateMap;
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
private notificationType = NotificationType;
private notificationTypes = Object.keys(NotificationType) as Array<NotificationType>;
private dirty = false;
@ViewChild('notificationTypeInput') notificationTypeInput: ElementRef<HTMLInputElement>;
constructor(@Inject(NOTIFICATION_TYPE_FILTER_PANEL_DATA) public data: NotificationTypeFilterPanelData,
private overlayRef: OverlayRef) {
this.selectedNotificationTypes = this.data.notificationTypes;
this.dirty = true;
}
ngOnInit() {
this.filteredNotificationTypesList = this.searchControlName.valueChanges.pipe(
debounceTime(150),
map(value => {
this.searchText = value;
return this.notificationTypes.filter(type => !this.selectedNotificationTypes.includes(type))
.filter(type => value ? type.toUpperCase().startsWith(value.toUpperCase()) : true);
})
);
}
public update() {
this.data.notificationTypesUpdated(this.selectedNotificationTypes);
if (this.overlayRef) {
this.overlayRef.dispose();
}
}
cancel() {
if (this.overlayRef) {
this.overlayRef.dispose();
}
}
public reset() {
this.selectedNotificationTypes.length = 0;
this.searchControlName.updateValueAndValidity({emitEvent: true});
}
remove(type: NotificationType) {
const index = this.selectedNotificationTypes.indexOf(type);
if (index >= 0) {
this.selectedNotificationTypes.splice(index, 1);
this.searchControlName.updateValueAndValidity({emitEvent: true});
}
}
onFocus() {
if (this.dirty) {
this.searchControlName.updateValueAndValidity({emitEvent: true});
this.dirty = false;
}
}
private add(type: NotificationType): void {
this.selectedNotificationTypes.push(type);
}
chipAdd(event: MatChipInputEvent): void {
const value = (event.value || '').trim();
if (value && this.notificationType[value]) {
this.add(this.notificationType[value]);
this.clear('');
}
}
selected(event: MatAutocompleteSelectedEvent): void {
if (this.notificationType[event.option.value]) {
this.add(this.notificationType[event.option.value]);
}
this.clear('');
}
clear(value: string = '') {
this.notificationTypeInput.nativeElement.value = value;
this.searchControlName.patchValue(value, {emitEvent: true});
setTimeout(() => {
this.notificationTypeInput.nativeElement.blur();
this.notificationTypeInput.nativeElement.focus();
}, 0);
}
displayTypeFn(type?: string): string | undefined {
return type ? type : undefined;
}
}

54
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html

@ -0,0 +1,54 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-unread-notification-panel" [style.padding]="padding" [style]="backgroundStyle$ | async">
<div class="tb-unread-notification-overlay" [style]="overlayStyle"></div>
<ng-template #counter>
<div *ngIf="showCounter" class="notification-counter" [style.background-color]="counterBackground">
<span class="notification-counter-value" [style]="counterValueStyle">{{ count$ | async }}</span>
</div>
</ng-template>
<ng-container *ngTemplateOutlet="widgetTitlePanel; context:{ titleSuffixTemplate: counter }"></ng-container>
<div class="tb-unread-notification-content">
<ng-container *ngIf="loadNotification; else loadingNotification">
<div *ngIf="notifications.length; else emptyNotification" style="overflow: auto; width: 100%;">
<section style="min-height: 100px; overflow: auto; padding: 6px 0;">
<div *ngFor="let notification of notifications; let last = last; trackBy: trackById">
<tb-notification [notification]="notification"
(markAsRead)="markAsRead($event)">
</tb-notification>
</div>
</section>
</div>
</ng-container>
<ng-template #emptyNotification>
<div class="tb-no-notification-svg-color" style="height: 85%;">
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%">
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use>
</svg>
</div>
<span class="tb-no-notification-text" translate>notification.no-notifications-yet</span>
</ng-template>
<ng-template #loadingNotification>
<div class="tb-no-data-available" style="margin: 20px; gap: 16px;">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<div class="tb-no-data-text" translate>notification.loading-notifications</div>
</div>
</ng-template>
</div>
</div>

65
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss

@ -0,0 +1,65 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../../../scss/constants";
.tb-no-notification-svg-color {
color: $tb-primary-color;
}
.tb-unread-notification-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px 24px 24px 24px;
> div:not(.tb-unread-notification-overlay) {
z-index: 1;
}
div.tb-widget-title {
padding: 0;
}
.tb-unread-notification-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-unread-notification-content {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
.tb-no-notification-text {
text-align: center;
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.38);
}
}
.notification-counter {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 22px;
background-color: green;
border-radius: 7px;
}
}

284
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts

@ -0,0 +1,284 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
Injector,
Input,
NgZone,
OnDestroy,
OnInit,
StaticProvider,
TemplateRef,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models';
import { isDefined } from '@core/utils';
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { BehaviorSubject, fromEvent, Observable, ReplaySubject, Subscription } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import {
unreadNotificationDefaultSettings,
UnreadNotificationWidgetSettings
} from '@home/components/widget/lib/cards/unread-notification-widget.models';
import { Notification, NotificationRequest, NotificationType } from '@shared/models/notification.models';
import { NotificationSubscriber } from '@shared/models/telemetry/telemetry.models';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
import { distinctUntilChanged, map, share, skip, take, tap } from 'rxjs/operators';
import { Router } from '@angular/router';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { ComponentPortal } from '@angular/cdk/portal';
import {
NOTIFICATION_TYPE_FILTER_PANEL_DATA,
NotificationTypeFilterPanelComponent
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { selectUserDetails } from '@core/auth/auth.selectors';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-unread-notification-widget',
templateUrl: './unread-notification-widget.component.html',
styleUrls: ['unread-notification-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy {
settings: UnreadNotificationWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
showCounter = true;
counterValueStyle: ComponentStyle;
counterBackground: string;
notifications: Notification[];
loadNotification = false;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;
private counterValue: BehaviorSubject<number> = new BehaviorSubject(0);
count$ = this.counterValue.asObservable().pipe(
distinctUntilChanged(),
map((value) => value >= 100 ? '99+' : value),
tap(() => Promise.resolve().then(() => this.cd.markForCheck())),
share({
connector: () => new ReplaySubject(1)
})
);
private notificationTypes: Array<NotificationType> = [];
private notificationSubscriber: NotificationSubscriber;
private notificationCountSubscriber: Subscription;
private notification: Subscription;
private contentResize$: ResizeObserver;
private defaultDashboardFullscreen = false;
private viewAllAction: WidgetAction = {
name: 'widgets.notification.button-view-all',
show: !this.defaultDashboardFullscreen,
icon: 'open_in_new',
onAction: ($event) => {
this.viewAll($event);
}
};
private filterAction: WidgetAction = {
name: 'widgets.notification.button-filter',
show: true,
icon: 'filter_list',
onAction: ($event) => {
this.editNotificationTypeFilter($event);
}
};
private markAsReadAction: WidgetAction = {
name: 'widgets.notification.button-mark-read',
show: true,
icon: 'done_all',
onAction: ($event) => {
this.markAsAllRead($event);
}
};
constructor(private store: Store<AppState>,
private imagePipe: ImagePipe,
private notificationWsService: NotificationWebsocketService,
private sanitizer: DomSanitizer,
private router: Router,
private zone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.unreadNotificationWidget = this;
this.settings = {...unreadNotificationDefaultSettings, ...this.ctx.settings};
this.showCounter = this.settings.showCounter;
this.counterValueStyle = textStyle(this.settings.counterValueFont);
this.counterValueStyle.color = this.settings.counterValueColor;
this.counterBackground = this.settings.counterColor;
this.ctx.widgetActions = [this.viewAllAction, this.filterAction, this.markAsReadAction];
this.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true;
this.store.pipe(select(selectUserDetails), take(1)).subscribe(
user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen
);
this.filterAction.show = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true;
this.markAsReadAction.show = isDefined(this.settings.enableMarkAsRead) ? this.settings.enableMarkAsRead : true;
this.initSubscription();
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
}
ngOnDestroy() {
if (this.contentResize$) {
this.contentResize$.disconnect();
}
this.unsubscribeSubscription();
}
private initSubscription() {
this.notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(
this.notificationWsService, this.zone, this.settings.maxNotificationDisplay, this.notificationTypes);
this.notification = this.notificationSubscriber.notifications$.subscribe(value => {
if (Array.isArray(value)) {
this.loadNotification = true;
this.notifications = value;
this.cd.markForCheck();
}
});
this.notificationCountSubscriber = this.notificationSubscriber.notificationCount$.pipe(
skip(1),
).subscribe(value => this.counterValue.next(value));
this.notificationSubscriber.subscribe();
}
private unsubscribeSubscription() {
this.notificationSubscriber.unsubscribe();
this.notificationCountSubscriber.unsubscribe();
this.notification.unsubscribe();
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
markAsRead(id: string) {
const cmd = NotificationSubscriber.createMarkAsReadCommand(this.notificationWsService, [id]);
cmd.subscribe();
}
markAsAllRead($event: Event) {
if ($event) {
$event.stopPropagation();
}
const cmd = NotificationSubscriber.createMarkAllAsReadCommand(this.notificationWsService);
cmd.subscribe();
}
viewAll($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {});
}
trackById(index: number, item: NotificationRequest): string {
return item.id.id;
}
private editNotificationTypeFilter($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig({
panelClass: 'tb-panel-container',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
height: 'fit-content',
maxHeight: '75vh',
width: '100%',
maxWidth: 700
});
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(target as HTMLElement)
.withPositions(DEFAULT_OVERLAY_POSITIONS);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: NOTIFICATION_TYPE_FILTER_PANEL_DATA,
useValue: {
notificationTypes: this.notificationTypes,
notificationTypesUpdated: (notificationTypes: Array<NotificationType>) => {
this.notificationTypes = notificationTypes;
this.unsubscribeSubscription();
this.initSubscription();
}
}
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(NotificationTypeFilterPanelComponent,
this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
});
this.ctx.detectChanges();
}
}

59
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts

@ -0,0 +1,59 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models';
export interface UnreadNotificationWidgetSettings {
maxNotificationDisplay: number;
showCounter: boolean;
counterValueFont: Font;
counterValueColor: string;
counterColor: string;
enableViewAll: boolean;
enableFilter: boolean;
enableMarkAsRead: boolean;
background: BackgroundSettings;
padding: string;
}
export const unreadNotificationDefaultSettings: UnreadNotificationWidgetSettings = {
maxNotificationDisplay: 6,
showCounter: true,
counterValueFont: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '600',
lineHeight: ''
},
counterValueColor: '#fff',
counterColor: '#305680',
enableViewAll: true,
enableFilter: true,
enableMarkAsRead: true,
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '12px'
};

4
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts

@ -35,7 +35,6 @@ import {
TimeSeriesChartSeriesType, TimeSeriesChartSeriesType,
TimeSeriesChartSettings, TimeSeriesChartSettings,
TimeSeriesChartThreshold, TimeSeriesChartThreshold,
TimeSeriesChartTooltipWidgetSettings,
TimeSeriesChartXAxisSettings, TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
@ -48,6 +47,9 @@ import {
ChartFillSettings, ChartFillSettings,
ChartFillType ChartFillType
} from '@home/components/widget/lib/chart/chart.models'; } from '@home/components/widget/lib/chart/chart.models';
import {
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export interface BarChartWithLabelsWidgetSettings extends TimeSeriesChartTooltipWidgetSettings { export interface BarChartWithLabelsWidgetSettings extends TimeSeriesChartTooltipWidgetSettings {
dataZoom: boolean; dataZoom: boolean;

4
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts

@ -38,7 +38,6 @@ import {
TimeSeriesChartSettings, TimeSeriesChartSettings,
TimeSeriesChartThreshold, TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings, timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartTooltipWidgetSettings,
TimeSeriesChartVisualMapPiece, TimeSeriesChartVisualMapPiece,
TimeSeriesChartXAxisSettings, TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings TimeSeriesChartYAxisSettings
@ -54,6 +53,9 @@ import {
ChartLineType, ChartLineType,
ChartShape ChartShape
} from '@home/components/widget/lib/chart/chart.models'; } from '@home/components/widget/lib/chart/chart.models';
import {
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export interface RangeItem { export interface RangeItem {
index: number; index: number;

6
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-state.models.ts

@ -18,13 +18,15 @@ import {
TimeSeriesChartStateSettings, TimeSeriesChartStateSettings,
TimeSeriesChartStateSourceType, TimeSeriesChartStateSourceType,
TimeSeriesChartTicksFormatter, TimeSeriesChartTicksFormatter,
TimeSeriesChartTicksGenerator, TimeSeriesChartTicksGenerator
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart.models'; } from '@home/components/widget/lib/chart/time-series-chart.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { FormattedData } from '@shared/models/widget.models'; import { FormattedData } from '@shared/models/widget.models';
import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import { LabelFormatterCallback } from 'echarts'; import { LabelFormatterCallback } from 'echarts';
import {
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export class TimeSeriesChartStateValueConverter { export class TimeSeriesChartStateValueConverter {

263
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts

@ -0,0 +1,263 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { isFunction } from '@core/utils';
import { FormattedData } from '@shared/models/widget.models';
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models';
import {
TimeSeriesChartDataItem,
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { Renderer2, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { Interval } from '@shared/models/time/time.models';
export type TimeSeriesChartTooltipValueFormatFunction =
(value: any, latestData: FormattedData, units?: string, decimals?: number) => string;
export interface TimeSeriesChartTooltipWidgetSettings {
showTooltip: boolean;
tooltipTrigger?: TimeSeriesChartTooltipTrigger;
tooltipShowFocusedSeries?: boolean;
tooltipLabelFont: Font;
tooltipLabelColor: string;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipValueFormatter?: string | TimeSeriesChartTooltipValueFormatFunction;
tooltipShowDate: boolean;
tooltipDateInterval?: boolean;
tooltipDateFormat: DateFormatSettings;
tooltipDateFont: Font;
tooltipDateColor: string;
tooltipBackgroundColor: string;
tooltipBackgroundBlur: number;
}
export enum TimeSeriesChartTooltipTrigger {
point = 'point',
axis = 'axis'
}
export const tooltipTriggerTranslationMap = new Map<TimeSeriesChartTooltipTrigger, string>(
[
[TimeSeriesChartTooltipTrigger.point, 'tooltip.trigger-point'],
[TimeSeriesChartTooltipTrigger.axis, 'tooltip.trigger-axis']
]
);
interface TooltipItem {
param: CallbackDataParams;
dataItem: TimeSeriesChartDataItem;
}
interface TooltipParams {
items: TooltipItem[];
comparisonItems: TooltipItem[];
}
export const createTooltipValueFormatFunction =
(tooltipValueFormatter: string | TimeSeriesChartTooltipValueFormatFunction): TimeSeriesChartTooltipValueFormatFunction => {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
if (isFunction(tooltipValueFormatter)) {
tooltipValueFormatFunction = tooltipValueFormatter as TimeSeriesChartTooltipValueFormatFunction;
} else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) {
try {
tooltipValueFormatFunction =
new Function('value', 'latestData', tooltipValueFormatter) as TimeSeriesChartTooltipValueFormatFunction;
} catch (e) {
}
}
return tooltipValueFormatFunction;
};
export class TimeSeriesChartTooltip {
constructor(private renderer: Renderer2,
private sanitizer: DomSanitizer,
private settings: TimeSeriesChartTooltipWidgetSettings,
private tooltipDateFormat: DateFormatProcessor,
private valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction) {
}
formatted(params: CallbackDataParams[] | CallbackDataParams, focusedSeriesIndex: number,
series?: TimeSeriesChartDataItem[], interval?: Interval): HTMLElement {
if (!this.settings.showTooltip) {
return undefined;
}
const tooltipParams = TimeSeriesChartTooltip.mapTooltipParams(params, series, focusedSeriesIndex);
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null;
}
const tooltipElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(tooltipElement, 'display', 'flex');
this.renderer.setStyle(tooltipElement, 'flex-direction', 'column');
this.renderer.setStyle(tooltipElement, 'align-items', 'flex-start');
this.renderer.setStyle(tooltipElement, 'gap', '16px');
this.buildItemsTooltip(tooltipElement, tooltipParams.items, interval);
this.buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, interval);
return tooltipElement;
}
private buildItemsTooltip(tooltipElement: HTMLElement,
items: TooltipItem[], interval?: Interval) {
if (items.length) {
const tooltipItemsElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(tooltipItemsElement, 'display', 'flex');
this.renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column');
this.renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start');
this.renderer.setStyle(tooltipItemsElement, 'gap', '4px');
this.renderer.appendChild(tooltipElement, tooltipItemsElement);
if (this.settings.tooltipShowDate) {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval));
}
for (const item of items) {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item));
}
}
}
private constructTooltipDateElement(param: CallbackDataParams, interval?: Interval): HTMLElement {
const dateElement: HTMLElement = this.renderer.createElement('div');
let dateText: string;
const startTs = param.value[2];
const endTs = param.value[3];
if (this.settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = this.tooltipDateFormat.update(startTs, interval);
const endDateText = this.tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = param.value[0];
dateText = this.tooltipDateFormat.update(ts, interval);
}
this.renderer.appendChild(dateElement, this.renderer.createText(dateText));
this.renderer.setStyle(dateElement, 'font-family', this.settings.tooltipDateFont.family);
this.renderer.setStyle(dateElement, 'font-size', this.settings.tooltipDateFont.size + this.settings.tooltipDateFont.sizeUnit);
this.renderer.setStyle(dateElement, 'font-style', this.settings.tooltipDateFont.style);
this.renderer.setStyle(dateElement, 'font-weight', this.settings.tooltipDateFont.weight);
this.renderer.setStyle(dateElement, 'line-height', this.settings.tooltipDateFont.lineHeight);
this.renderer.setStyle(dateElement, 'color', this.settings.tooltipDateColor);
return dateElement;
}
private constructTooltipSeriesElement(item: TooltipItem): HTMLElement {
const labelValueElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(labelValueElement, 'display', 'flex');
this.renderer.setStyle(labelValueElement, 'flex-direction', 'row');
this.renderer.setStyle(labelValueElement, 'align-items', 'center');
this.renderer.setStyle(labelValueElement, 'align-self', 'stretch');
this.renderer.setStyle(labelValueElement, 'gap', '12px');
const labelElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(labelElement, 'display', 'flex');
this.renderer.setStyle(labelElement, 'align-items', 'center');
this.renderer.setStyle(labelElement, 'gap', '8px');
this.renderer.appendChild(labelValueElement, labelElement);
const circleElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(circleElement, 'width', '8px');
this.renderer.setStyle(circleElement, 'height', '8px');
this.renderer.setStyle(circleElement, 'border-radius', '50%');
this.renderer.setStyle(circleElement, 'background', item.param.color);
this.renderer.appendChild(labelElement, circleElement);
const labelTextElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setProperty(labelTextElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, item.param.seriesName));
this.renderer.setStyle(labelTextElement, 'font-family', this.settings.tooltipLabelFont.family);
this.renderer.setStyle(labelTextElement, 'font-size', this.settings.tooltipLabelFont.size + this.settings.tooltipLabelFont.sizeUnit);
this.renderer.setStyle(labelTextElement, 'font-style', this.settings.tooltipLabelFont.style);
this.renderer.setStyle(labelTextElement, 'font-weight', this.settings.tooltipLabelFont.weight);
this.renderer.setStyle(labelTextElement, 'line-height', this.settings.tooltipLabelFont.lineHeight);
this.renderer.setStyle(labelTextElement, 'color', this.settings.tooltipLabelColor);
this.renderer.appendChild(labelElement, labelTextElement);
const valueElement: HTMLElement = this.renderer.createElement('div');
let formatFunction = this.valueFormatFunction;
let latestData: FormattedData;
let units = '';
let decimals = 0;
if (item.dataItem) {
if (item.dataItem.tooltipValueFormatFunction) {
formatFunction = item.dataItem.tooltipValueFormatFunction;
}
latestData = item.dataItem.latestData;
units = item.dataItem.units;
decimals = item.dataItem.decimals;
}
if (!latestData) {
latestData = {} as FormattedData;
}
const value = formatFunction(item.param.value[1], latestData, units, decimals);
this.renderer.setProperty(valueElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, value));
this.renderer.setStyle(valueElement, 'flex', '1');
this.renderer.setStyle(valueElement, 'text-align', 'end');
this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family);
this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit);
this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style);
this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight);
this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight);
this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor);
this.renderer.appendChild(labelValueElement, valueElement);
return labelValueElement;
}
private static mapTooltipParams(params: CallbackDataParams[] | CallbackDataParams,
series?: TimeSeriesChartDataItem[],
focusedSeriesIndex?: number): TooltipParams {
const result: TooltipParams = {
items: [],
comparisonItems: []
};
if (!params || Array.isArray(params) && !params[0]) {
return result;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return result;
}
let seriesParams: CallbackDataParams = null;
if (Array.isArray(params) && focusedSeriesIndex > -1) {
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex);
} else if (!Array.isArray(params)) {
seriesParams = params;
}
if (seriesParams) {
TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series);
} else if (Array.isArray(params)) {
for (seriesParams of params) {
TimeSeriesChartTooltip.appendTooltipItem(result, seriesParams, series);
}
}
return result;
}
private static appendTooltipItem(tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: TimeSeriesChartDataItem[]) {
const dataItem = series?.find(s => s.id === seriesParams.seriesId);
const tooltipItem: TooltipItem = {
param: seriesParams,
dataItem
};
if (dataItem?.comparisonItem) {
tooltipParams.comparisonItems.push(tooltipItem);
} else {
tooltipParams.items.push(tooltipItem);
}
};
}

26
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html

@ -74,20 +74,20 @@
<th> <th>
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container> <ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container>
</th> </th>
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"> <td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
{{ legendData.data[legendKey.dataIndex].min }} [innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'">
</td> </td>
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"> <td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
{{ legendData.data[legendKey.dataIndex].max }} [innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'">
</td> </td>
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"> <td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
{{ legendData.data[legendKey.dataIndex].avg }} [innerHTML]="legendData.data[legendKey.dataIndex].avg | safe: 'html'">
</td> </td>
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"> <td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
{{ legendData.data[legendKey.dataIndex].total }} [innerHTML]="legendData.data[legendKey.dataIndex].total | safe: 'html'">
</td> </td>
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"> <td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
{{ legendData.data[legendKey.dataIndex].latest }} [innerHTML]="legendData.data[legendKey.dataIndex].latest | safe: 'html'">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -106,15 +106,15 @@
(mouseleave)="onLegendKeyLeave(legendKey)" (mouseleave)="onLegendKeyLeave(legendKey)"
(click)="toggleLegendKey(legendKey)"> (click)="toggleLegendKey(legendKey)">
<div class="tb-time-series-chart-legend-item-label-circle" [style]="{background: !legendKey.dataKey.hidden ? legendKey.dataKey.color : null}"></div> <div class="tb-time-series-chart-legend-item-label-circle" [style]="{background: !legendKey.dataKey.hidden ? legendKey.dataKey.color : null}"></div>
<div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle">{{ legendKey.dataKey.label }}</div> <div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle" [innerHTML]="legendKey.dataKey.label | safe: 'html'"></div>
</div> </div>
</div> </div>
</ng-template> </ng-template>
<ng-template #legendDataRow let-label="label" let-type="type"> <ng-template #legendDataRow let-label="label" let-type="type">
<tr> <tr>
<th class="tb-time-series-chart-legend-type-label" [style]="legendColumnTitleStyle">{{ label | translate }}</th> <th class="tb-time-series-chart-legend-type-label" [style]="legendColumnTitleStyle">{{ label | translate }}</th>
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"> <td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
{{ legendData.data[legendKey.dataIndex][type] }} [innerHTML]="legendData.data[legendKey.dataIndex][type] | safe: 'html'">
</td> </td>
</tr> </tr>
</ng-template> </ng-template>

252
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

@ -23,15 +23,12 @@ import {
autoDateFormat, autoDateFormat,
AutoDateFormatSettings, AutoDateFormatSettings,
ComponentStyle, ComponentStyle,
DateFormatProcessor,
DateFormatSettings,
Font, Font,
tsToFormatTimeUnit, tsToFormatTimeUnit,
ValueSourceConfig, ValueSourceConfig,
ValueSourceType ValueSourceType
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { import {
CallbackDataParams,
TimeAxisBandWidthCalculator, TimeAxisBandWidthCalculator,
VisualMapComponentOption, VisualMapComponentOption,
XAXisOption, XAXisOption,
@ -75,12 +72,10 @@ import { BuiltinTextPosition } from 'zrender/src/core/types';
import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel'; import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel';
import { import {
calculateAggIntervalWithWidgetTimeWindow, calculateAggIntervalWithWidgetTimeWindow,
Interval,
IntervalMath, IntervalMath,
WidgetTimewindow WidgetTimewindow
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { Renderer2 } from '@angular/core';
import { import {
chartAnimationDefaultSettings, chartAnimationDefaultSettings,
ChartAnimationSettings, ChartAnimationSettings,
@ -98,6 +93,11 @@ import {
prepareChartThemeColor prepareChartThemeColor
} from '@home/components/widget/lib/chart/chart.models'; } from '@home/components/widget/lib/chart/chart.models';
import { BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries'; import { BarSeriesLabelOption } from 'echarts/types/src/chart/bar/BarSeries';
import {
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction,
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
type TimeSeriesChartDataEntry = [number, any, number, number]; type TimeSeriesChartDataEntry = [number, any, number, number];
@ -127,9 +127,6 @@ const toTimeSeriesChartDataEntry = (entry: DataEntry, valueConverter?: (value: a
return item; return item;
}; };
export type TimeSeriesChartTooltipValueFormatFunction =
(value: any, latestData: FormattedData, units?: string, decimals?: number) => string;
export interface TimeSeriesChartDataItem { export interface TimeSeriesChartDataItem {
id: string; id: string;
datasource: Datasource; datasource: Datasource;
@ -216,245 +213,6 @@ export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption,
timeAxisOption.max = (typeof max !== 'undefined' && Math.abs(max - defaultMax) < 1000) ? max : defaultMax; timeAxisOption.max = (typeof max !== 'undefined' && Math.abs(max - defaultMax) < 1000) ? max : defaultMax;
}; };
export enum TimeSeriesChartTooltipTrigger {
point = 'point',
axis = 'axis'
}
export const tooltipTriggerTranslationMap = new Map<TimeSeriesChartTooltipTrigger, string>(
[
[ TimeSeriesChartTooltipTrigger.point, 'tooltip.trigger-point' ],
[ TimeSeriesChartTooltipTrigger.axis, 'tooltip.trigger-axis' ]
]
);
export interface TimeSeriesChartTooltipWidgetSettings {
showTooltip: boolean;
tooltipTrigger?: TimeSeriesChartTooltipTrigger;
tooltipShowFocusedSeries?: boolean;
tooltipLabelFont: Font;
tooltipLabelColor: string;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipValueFormatter?: string | TimeSeriesChartTooltipValueFormatFunction;
tooltipShowDate: boolean;
tooltipDateInterval?: boolean;
tooltipDateFormat: DateFormatSettings;
tooltipDateFont: Font;
tooltipDateColor: string;
tooltipBackgroundColor: string;
tooltipBackgroundBlur: number;
}
export const createTooltipValueFormatFunction =
(tooltipValueFormatter: string | TimeSeriesChartTooltipValueFormatFunction): TimeSeriesChartTooltipValueFormatFunction => {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
if (isFunction(tooltipValueFormatter)) {
tooltipValueFormatFunction = tooltipValueFormatter as TimeSeriesChartTooltipValueFormatFunction;
} else if (typeof tooltipValueFormatter === 'string' && tooltipValueFormatter.length) {
try {
tooltipValueFormatFunction =
new Function('value', 'latestData', tooltipValueFormatter) as TimeSeriesChartTooltipValueFormatFunction;
} catch (e) {}
}
return tooltipValueFormatFunction;
};
export const timeSeriesChartTooltipFormatter = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: TimeSeriesChartTooltipWidgetSettings,
params: CallbackDataParams[] | CallbackDataParams,
valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction,
focusedSeriesIndex: number,
series?: TimeSeriesChartDataItem[],
interval?: Interval): null | HTMLElement => {
const tooltipParams = mapTooltipParams(params, series, focusedSeriesIndex);
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null;
}
const tooltipElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipElement, 'display', 'flex');
renderer.setStyle(tooltipElement, 'flex-direction', 'column');
renderer.setStyle(tooltipElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipElement, 'gap', '16px');
buildItemsTooltip(tooltipElement, tooltipParams.items, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
buildItemsTooltip(tooltipElement, tooltipParams.comparisonItems, renderer, tooltipDateFormat, settings, valueFormatFunction, interval);
return tooltipElement;
};
interface TooltipItem {
param: CallbackDataParams;
dataItem: TimeSeriesChartDataItem;
}
interface TooltipParams {
items: TooltipItem[];
comparisonItems: TooltipItem[];
}
const buildItemsTooltip = (tooltipElement: HTMLElement,
items: TooltipItem[],
renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: TimeSeriesChartTooltipWidgetSettings,
valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction,
interval?: Interval) => {
if (items.length) {
const tooltipItemsElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(tooltipItemsElement, 'display', 'flex');
renderer.setStyle(tooltipItemsElement, 'flex-direction', 'column');
renderer.setStyle(tooltipItemsElement, 'align-items', 'flex-start');
renderer.setStyle(tooltipItemsElement, 'gap', '4px');
renderer.appendChild(tooltipElement, tooltipItemsElement);
if (settings.tooltipShowDate) {
renderer.appendChild(tooltipItemsElement,
constructTooltipDateElement(renderer, tooltipDateFormat, settings, items[0].param, interval));
}
for (const item of items) {
renderer.appendChild(tooltipItemsElement,
constructTooltipSeriesElement(renderer, settings, item, valueFormatFunction));
}
}
};
const mapTooltipParams = (params: CallbackDataParams[] | CallbackDataParams,
series?: TimeSeriesChartDataItem[],
focusedSeriesIndex?: number): TooltipParams => {
const result: TooltipParams = {
items: [],
comparisonItems: []
};
if (!params || Array.isArray(params) && !params[0]) {
return result;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return result;
}
let seriesParams: CallbackDataParams = null;
if (Array.isArray(params) && focusedSeriesIndex > -1) {
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex);
} else if (!Array.isArray(params)) {
seriesParams = params;
}
if (seriesParams) {
appendTooltipItem(result, seriesParams, series);
} else if (Array.isArray(params)) {
for (seriesParams of params) {
appendTooltipItem(result, seriesParams, series);
}
}
return result;
};
const appendTooltipItem = (tooltipParams: TooltipParams, seriesParams: CallbackDataParams, series?: TimeSeriesChartDataItem[]) => {
const dataItem = series?.find(s => s.id === seriesParams.seriesId);
const tooltipItem: TooltipItem = {
param: seriesParams,
dataItem
};
if (dataItem?.comparisonItem) {
tooltipParams.comparisonItems.push(tooltipItem);
} else {
tooltipParams.items.push(tooltipItem);
}
};
const constructTooltipDateElement = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: TimeSeriesChartTooltipWidgetSettings,
param: CallbackDataParams,
interval?: Interval): HTMLElement => {
const dateElement: HTMLElement = renderer.createElement('div');
let dateText: string;
const startTs = param.value[2];
const endTs = param.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = param.value[0];
dateText = tooltipDateFormat.update(ts, interval);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
renderer.setStyle(dateElement, 'font-size', settings.tooltipDateFont.size + settings.tooltipDateFont.sizeUnit);
renderer.setStyle(dateElement, 'font-style', settings.tooltipDateFont.style);
renderer.setStyle(dateElement, 'font-weight', settings.tooltipDateFont.weight);
renderer.setStyle(dateElement, 'line-height', settings.tooltipDateFont.lineHeight);
renderer.setStyle(dateElement, 'color', settings.tooltipDateColor);
return dateElement;
};
const constructTooltipSeriesElement = (renderer: Renderer2,
settings: TimeSeriesChartTooltipWidgetSettings,
item: TooltipItem,
valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction): HTMLElement => {
const labelValueElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelValueElement, 'display', 'flex');
renderer.setStyle(labelValueElement, 'flex-direction', 'row');
renderer.setStyle(labelValueElement, 'align-items', 'center');
renderer.setStyle(labelValueElement, 'align-self', 'stretch');
renderer.setStyle(labelValueElement, 'gap', '12px');
const labelElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelElement, 'display', 'flex');
renderer.setStyle(labelElement, 'align-items', 'center');
renderer.setStyle(labelElement, 'gap', '8px');
renderer.appendChild(labelValueElement, labelElement);
const circleElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(circleElement, 'width', '8px');
renderer.setStyle(circleElement, 'height', '8px');
renderer.setStyle(circleElement, 'border-radius', '50%');
renderer.setStyle(circleElement, 'background', item.param.color);
renderer.appendChild(labelElement, circleElement);
const labelTextElement: HTMLElement = renderer.createElement('div');
renderer.appendChild(labelTextElement, renderer.createText(item.param.seriesName));
renderer.setStyle(labelTextElement, 'font-family', settings.tooltipLabelFont.family);
renderer.setStyle(labelTextElement, 'font-size', settings.tooltipLabelFont.size + settings.tooltipLabelFont.sizeUnit);
renderer.setStyle(labelTextElement, 'font-style', settings.tooltipLabelFont.style);
renderer.setStyle(labelTextElement, 'font-weight', settings.tooltipLabelFont.weight);
renderer.setStyle(labelTextElement, 'line-height', settings.tooltipLabelFont.lineHeight);
renderer.setStyle(labelTextElement, 'color', settings.tooltipLabelColor);
renderer.appendChild(labelElement, labelTextElement);
const valueElement: HTMLElement = renderer.createElement('div');
let formatFunction = valueFormatFunction;
let latestData: FormattedData;
let units = '';
let decimals = 0;
if (item.dataItem) {
if (item.dataItem.tooltipValueFormatFunction) {
formatFunction = item.dataItem.tooltipValueFormatFunction;
}
latestData = item.dataItem.latestData;
units = item.dataItem.units;
decimals = item.dataItem.decimals;
}
if (!latestData) {
latestData = {} as FormattedData;
}
const value = formatFunction(item.param.value[1], latestData, units, decimals);
renderer.appendChild(valueElement, renderer.createText(value));
renderer.setStyle(valueElement, 'flex', '1');
renderer.setStyle(valueElement, 'text-align', 'end');
renderer.setStyle(valueElement, 'font-family', settings.tooltipValueFont.family);
renderer.setStyle(valueElement, 'font-size', settings.tooltipValueFont.size + settings.tooltipValueFont.sizeUnit);
renderer.setStyle(valueElement, 'font-style', settings.tooltipValueFont.style);
renderer.setStyle(valueElement, 'font-weight', settings.tooltipValueFont.weight);
renderer.setStyle(valueElement, 'line-height', settings.tooltipValueFont.lineHeight);
renderer.setStyle(valueElement, 'color', settings.tooltipValueColor);
renderer.appendChild(labelValueElement, valueElement);
return labelValueElement;
};
export enum TimeSeriesChartType { export enum TimeSeriesChartType {
default = 'default', default = 'default',

40
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -21,7 +21,6 @@ import {
createTimeSeriesVisualMapOption, createTimeSeriesVisualMapOption,
createTimeSeriesXAxis, createTimeSeriesXAxis,
createTimeSeriesYAxis, createTimeSeriesYAxis,
createTooltipValueFormatFunction,
defaultTimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings,
generateChartData, generateChartData,
LineSeriesStepType, LineSeriesStepType,
@ -37,9 +36,6 @@ import {
TimeSeriesChartThreshold, TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings, timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartThresholdItem, TimeSeriesChartThresholdItem,
timeSeriesChartTooltipFormatter,
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction,
TimeSeriesChartType, TimeSeriesChartType,
TimeSeriesChartXAxis, TimeSeriesChartXAxis,
TimeSeriesChartYAxis, TimeSeriesChartYAxis,
@ -74,6 +70,12 @@ import { DeepPartial } from '@shared/models/common';
import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models'; import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models';
import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models'; import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models';
import { ChartLabelPosition, ChartShape, toAnimationOption } from '@home/components/widget/lib/chart/chart.models'; import { ChartLabelPosition, ChartShape, toAnimationOption } from '@home/components/widget/lib/chart/chart.models';
import {
createTooltipValueFormatFunction,
TimeSeriesChartTooltip,
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export class TbTimeSeriesChart { export class TbTimeSeriesChart {
@ -133,7 +135,7 @@ export class TbTimeSeriesChart {
private timeSeriesChartOptions: EChartsOption; private timeSeriesChartOptions: EChartsOption;
private readonly tooltipDateFormat: DateFormatProcessor; private readonly tooltipDateFormat: DateFormatProcessor;
private readonly tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction; private readonly timeSeriesChartTooltip: TimeSeriesChartTooltip;
private readonly stateValueConverter: TimeSeriesChartStateValueConverter; private readonly stateValueConverter: TimeSeriesChartStateValueConverter;
private yMinSubject = new BehaviorSubject(-1); private yMinSubject = new BehaviorSubject(-1);
@ -162,6 +164,8 @@ export class TbTimeSeriesChart {
private renderer: Renderer2, private renderer: Renderer2,
private autoResize = true) { private autoResize = true) {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
this.settings = mergeDeep({} as TimeSeriesChartSettings, this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings, timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings); this.inputSettings as TimeSeriesChartSettings);
@ -169,7 +173,7 @@ export class TbTimeSeriesChart {
this.stackMode = !this.comparisonEnabled && this.settings.stack; this.stackMode = !this.comparisonEnabled && this.settings.stack;
if (this.settings.states && this.settings.states.length) { if (this.settings.states && this.settings.states.length) {
this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states); this.stateValueConverter = new TimeSeriesChartStateValueConverter(this.ctx.utilsService, this.settings.states);
this.tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter; tooltipValueFormatFunction = this.stateValueConverter.tooltipFormatter;
} }
const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page'); const $dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null; const dashboardPageElement = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null;
@ -183,14 +187,20 @@ export class TbTimeSeriesChart {
if (this.settings.tooltipShowDate) { if (this.settings.tooltipShowDate) {
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
} }
if (!this.tooltipValueFormatFunction) { if (!tooltipValueFormatFunction) {
this.tooltipValueFormatFunction = tooltipValueFormatFunction = createTooltipValueFormatFunction(this.settings.tooltipValueFormatter);
createTooltipValueFormatFunction(this.settings.tooltipValueFormatter); if (!tooltipValueFormatFunction) {
if (!this.tooltipValueFormatFunction) { tooltipValueFormatFunction = (value, _latestData, units, decimals) => formatValue(value, decimals, units, false);
this.tooltipValueFormatFunction = (value, _latestData, units, decimals) => formatValue(value, decimals, units, false);
} }
} }
} }
this.timeSeriesChartTooltip = new TimeSeriesChartTooltip(
this.renderer,
this.ctx.sanitizer,
this.settings,
this.tooltipDateFormat,
tooltipValueFormatFunction
);
this.onResize(); this.onResize();
if (this.autoResize) { if (this.autoResize) {
this.shapeResize$ = new ResizeObserver(() => { this.shapeResize$ = new ResizeObserver(() => {
@ -603,10 +613,12 @@ export class TbTimeSeriesChart {
type: this.noAggregation ? 'line' : 'shadow' type: this.noAggregation ? 'line' : 'shadow'
}, },
formatter: (params: CallbackDataParams[]) => formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? timeSeriesChartTooltipFormatter(this.renderer, this.tooltipDateFormat, this.timeSeriesChartTooltip.formatted(
this.settings, params, this.tooltipValueFormatFunction, params,
this.settings.tooltipShowFocusedSeries ? getFocusedSeriesIndex(this.timeSeriesChart) : -1, this.settings.tooltipShowFocusedSeries ? getFocusedSeriesIndex(this.timeSeriesChart) : -1,
this.dataItems, this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined, this.dataItems,
this.noAggregation ? null : this.ctx.timeWindow.interval,
),
padding: [8, 12], padding: [8, 12],
backgroundColor: this.settings.tooltipBackgroundColor, backgroundColor: this.settings.tooltipBackgroundColor,
borderWidth: 0, borderWidth: 0,

86
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html

@ -0,0 +1,86 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="brokerConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="brokerConfigFormGroup.get('host').hasError('required')
&& brokerConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') ||
brokerConfigFormGroup.get('port').hasError('max')) &&
brokerConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.mqtt-version</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.client-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('clientId')"
*ngIf="!brokerConfigFormGroup.get('clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</div>
</div>
<tb-security-config formControlName="security">
</tb-security-config>
</div>

130
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts

@ -0,0 +1,130 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerConfig,
MqttVersions,
noLeadTrailSpacesRegex,
PortLimits,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { generateSecret } from '@core/utils';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-broker-config-control',
templateUrl: './broker-config-control.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BrokerConfigControlComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BrokerConfigControlComponent),
multi: true
}
]
})
export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, OnDestroy {
brokerConfigFormGroup: UntypedFormGroup;
mqttVersions = MqttVersions;
portLimits = PortLimits;
private onChange: (value: string) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private translate: TranslateService) {
this.brokerConfigFormGroup = this.fb.group({
name: ['', []],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
security: []
});
this.brokerConfigFormGroup.valueChanges.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
get portErrorTooltip(): string {
if (this.brokerConfigFormGroup.get('port').hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
this.brokerConfigFormGroup.get('port').hasError('min') ||
this.brokerConfigFormGroup.get('port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
generate(formControlName: string): void {
this.brokerConfigFormGroup.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(brokerConfig: BrokerConfig): void {
this.brokerConfigFormGroup.patchValue(brokerConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.brokerConfigFormGroup.valid ? null : {
brokerConfigFormGroup: {valid: false}
};
}
}

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html

@ -50,9 +50,10 @@
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
<div matSuffix <div *ngIf="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
matSuffix
class="see-example" class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" [tb-help-popup]="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
tb-help-popup-placement="left" tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}"> [tb-help-popup-style]="{maxWidth: '970px'}">
</div> </div>
@ -83,9 +84,10 @@
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
<div matSuffix <div *ngIf="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
matSuffix
class="see-example" class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'" [tb-help-popup]="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
tb-help-popup-placement="left" tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}"> [tb-help-popup-style]="{maxWidth: '970px'}">
</div> </div>

0
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss

25
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts

@ -16,27 +16,19 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
ElementRef,
forwardRef, forwardRef,
Input, Input,
NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -50,6 +42,7 @@ import {
import { import {
DeviceInfoType, DeviceInfoType,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
OPCUaSourceTypes,
SourceTypes, SourceTypes,
SourceTypeTranslationsMap SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -88,7 +81,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
required = false; required = false;
@Input() @Input()
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes); sourceTypes: Array<SourceTypes | OPCUaSourceTypes> = Object.values(SourceTypes);
deviceInfoTypeValue: any; deviceInfoTypeValue: any;
@ -111,14 +104,6 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
public translate: TranslateService, public translate: TranslateService,
public dialog: MatDialog, public dialog: MatDialog,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private dialogService: DialogService,
private entityService: EntityService,
private utils: UtilsService,
private zone: NgZone,
private cd: ChangeDetectorRef,
private elementRef: ElementRef,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
@ -131,13 +116,13 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
if (this.useSource) { if (this.useSource) {
this.mappingFormGroup.addControl('deviceNameExpressionSource', this.mappingFormGroup.addControl('deviceNameExpressionSource',
this.fb.control(SourceTypes.MSG, [])); this.fb.control(this.sourceTypes[0], []));
} }
if (this.deviceInfoType === DeviceInfoType.FULL) { if (this.deviceInfoType === DeviceInfoType.FULL) {
if (this.useSource) { if (this.useSource) {
this.mappingFormGroup.addControl('deviceProfileExpressionSource', this.mappingFormGroup.addControl('deviceProfileExpressionSource',
this.fb.control(SourceTypes.MSG, [])); this.fb.control(this.sourceTypes[0], []));
} }
this.mappingFormGroup.addControl('deviceProfileExpression', this.mappingFormGroup.addControl('deviceProfileExpression',
this.fb.control('', this.required ? this.fb.control('', this.required ?
@ -154,6 +139,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
super.ngOnDestroy();
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -175,5 +161,4 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
updateView(value: any) { updateView(value: any) {
this.propagateChange(value); this.propagateChange(value);
} }
} }

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

Loading…
Cancel
Save