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_value_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",
"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",
"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": {
"type": "latest",
"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",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMkAAACgCAMAAACR47ilAAAA9lBMVEX////39/f5+fkAAAD////////////9/f36+vr///8Ihyv/X2r8/Pzy8vL39/f5+fnu7u4hISHs7Oz09PTq6urm5ubg8OXo6OgXjjjw9/Ki0q8nlkWrq6uDw5VktHr/6+3/c32Ty6LB4cqenp7/9fZ0dHQnlkbj4+N0vIf/19r/r7RFpWA2nlP/h4/R6dc9PT3b29v/aXNGpWBYWFgvLy//w8eQkJCy2r1VrG3IyMj/pavV1dXOzs7/m6LCwsL/ub7/fYb/4eP/zdG6urqCgoK2trb/kZhmZmY8PDykpKRKSkr/9fW728TO4tSDwpWi0rCFw5ZWrG24QEw9AAAACXRSTlOAgIAAcN+QOHDzcMjxAAALEklEQVR42tyae3PSQBTFq/V122ySbrIJYIBSUB4KgooKpWBRO75n/P5fxt2FugkLIZvNMuqZ0Zb/+M05596b6NHx8b378K/r/r3j46PjB/A/6MHx0UP4P/Tw6Aj+Dz06HIl1KzCiuwch4RTOWkZwDkji2J6HmDybyike5s5BSBgHcjH2Mcau6yLPK5rGNIkAQa7vehQH45CEPsXh5jiFsZgmESAYO7CWY7t4RVMgjFGS4G2lU2sEtCO2hzFAa3E1XXSvgYuykRVMMSymSIJK52XpZKWqxUywYDw6Xak/HXMcy/NJiDmLo81iYnaVK7XHJ3E1qCU2dE/jOr8Z1zlMSJ0pwpfCPSk3Xp5sqmQj34L3p5t6v2AwjstShmxbY5Jpk8huMAxZPxCG1uk29ZkzFgpDjJCGLwWnK6iWTrbru+tC/XSHrpgxHmPxbPW6GPDkImaHTOILT2T1uwCAQp93PyeKPonMIesx7QlA/3S3zscAFiaURQHFBElAOVJUsRFGfHal+HJNWXytiOn3pNw5SdVPy/FovGCcisL74rGI5UTRJ2mU0jBKtQu24xFDqV+dp6GMeixiIVYaYurpUg9W6VmnMijfXvQI+/zLXS97/dHOulBbEMEuTdhhSYQhMkVjsOUWtmGl6/HNDhpmi3rC9NNV/rUjUGVIirtCbSF8PAFTd3vU3tdZwtRQ9D0ZPN3C8awiMJIolMV1se+Ttr+iWd5sS9iSJ0wBRZ+kscWOzgB2yXIoDHv+5TQhtmHHDOipoeiTdGSOahl2y6JiMBzHZUlzLYBvY5ll2gInVEDRIynX1DgEDedZw/AZ0DvfVhYFFC2S8vNNkHcBZJQFaxjX9Ylvs4xtGccWe2xxjJMEm11/fAEq+uMMY7Eoy7mEouyKINEB6YhgKcFwFt6X3hYUqfbaJDKIliFCtywhYrZIKLz25kjkjryTDFFkcXFImC0Sik3SUPR3/AZIA3TEVyaLGAJYjJIoLUDE9WQUBU8U9kgpV7LkiGHals2E9QHYi0vHEEk1WZEA9CRu5ZBIKFMA3095jNQieVs4iLiVQ4qSfJe0ACutKjokwVMTIMBfuNKy2NBKolyDTVxkGyAxAiISRjyGkmy9G7pp+cpJUjUDwrUd5Yo9ee3Ll/oUHhgEEa4kaz9m+fIcBRLlbA2gSAkUB+rxvTKi+fL35ksxXVX1hageMJ9YyVdjN6tbch+JiidBHKQKxUugLOIoXbDD/flS8eR5vCRgRBZ7yRf6ADfJ+eXvN0XBk0r8RlFou/peCV1oxVvfy1J6BU+emi2JCBjCxIZusvT+3knMSZQtqYEBCVcQu1umCVMsZoo2iWyJmWyJ1ot8xSYxsnVJZEuqYFSs9cSGpVpTMpM8NTq35HwRC/pSU/RIZEsqYFIiX90NU9IXPZ/Cf5clK1OwbApOjVdGTy7MWiKz2LThCVOWgNKfHjOS1MxbIi8ViJvSB4ukmpItXWXzlsj5CpOm1HnndT2pmLdEnsQbTemBl/rwmI3kmXFLZFP4+FrG7ki+5zVJAmm9mxe/WaA1Eijd9HjxniiE6x0cSqtFP03GS5fkXbHhIplNSWzHvhQvdZKSIClDRk0+cbVBEmpmH8QWxOLFl2OKKft7cpEnXJiQaEKIc+tDU5nE2ozXmB3EWiSNnOGKhpxhNkSAZ82hA87lrC1IlOM1BTvMSSLXJFAmmUWzeYTIvDlDKJrPoktKorBSxPQSczg/SUl5LQoS1EQAnyY8XZh+HkaMJPP0Si7HFoTYc3I0Xt4mNWWSdgT86/OekMn8kwLJ6mJZxDcKxsjO78lb9ZoIkqEgoVRtdKlG4uN4URaA+IuJvCRVQTJQJsFfHID5nJNM5gAKJLwoIbQEyRU7vfJ4IhcelElgHg1nXxCg5iUafrmcfeEkSkU5F/91glXedpR7Ip+Pz0FJl5j9PZxPEP+B6J8ZmjnOTOF5izix15EjMbxyeVLSP7o07mEPponhpUMiwtUB05KHlwvj+NNWiPOTBHrvUDVJMIblxhhWJ5GvrrdgWjKJD9fJy6sQkgswL/leqcdI+ELJS1Ix8O9xSgsFBEmvKJIAlNT+80sUTWAt9CFiUznzYd/eIAkPTtJ+8/rJ+rdXZ1QvIs7x8TX78ARl9EQi8Q7sCWqyb78meXF29pV9ZLbQn69ffT47e5V9yUPssXF6cBLuwpok+t3e2fYkDgRx/DT3ZhpBil7dglSBQJtrrQnWqjHmHjBivMfv/2muHU4HqZDtTLca4j8xAaOmP3dmOjudHWw7BfCT/Oqn+BpuEUvHuookBzWTnA0bp0iCl53b0si2AYanp35ub7Y9LEFSKK/U7PFLJDM7oYSsDMmre/wTyRVetp+/fVRq277+/UQehYsbrQGDBBdlloWse3++HmmaeVHKuzN+WUOCWb1u102HRdJ4sDMl14A6xFjAzFa+Y52ozgySSNCukts0i7zThTUZcba/WBoWZJAuI6snEgxaPgbmpAEoXJdDXZJnJYlveS7M3zMqKq1wSB7sGWSa5ovyqMQeaRdXnu+0PrX4awIRr9yFJHTVVzlJejb7/70z3d1vE04Wdr+Q736BS+KReXFI0KzQyK7x5j6/M97qdhgsnnw+gd0DSUWiTyQxg2SYZY/DwwzkHnOW0/QwTezE1wzCeMp2sTDcqrtyRySQ2qgHP/eWe3ydTEGL5KXKHYNEWE0dpo+X69/OZqPp41ZldjYaNko8//3MrqbKK9xykZvsLlW414QuxlMHnuR736/ipw79V6kT7RTu8Dfo8BU9nZsAT9yGoibc6GZdRKLlKMqFWkQp/TGRHGGLKqyQ1rNfz0CLBPsptmhNOgLzErgJGpd2Z4EWiWuR6jIvbIGeGxclwvtSEghl3Zy8Zu589sQ5owNHNw4rMC1yk124W+qKEvd3uYpdsJeQUEaf6Sg3Lnn3YMD2eQFJuwl3zyNXU04ysLjPHiS3k72F9PEcC6ny3lQI+YvCj10t6vU4BnwwVwFJR7AoEpc/f8pU9sQ93MUer7ELJkVnanDazMl8BNNOPu+vEpKe6IQAt5G7nU+aOPqRvcM9ViUkrhKfCOKcoG3P16GhMUUKSfQWpV6nd39HSnm/2vvtVv6116zsdJPbFZ9u4sxF+Nto5UMwm80Kz85dsE+ccUFQP5uZMo4qT2aGFikCs6ItUbizm2FUe4I5JhDTPSw9RspaggQCqyZXGVgMEtr9lnR6FYMpOWNGnMQ1Yf2vrLEhr0cQRuWgFAkERlFonAujgFuSBCJjKATCGydQlsRR5lAIhFG+JRKGqxhAQRBugNxmTI4xhILOzr1pyaf5VFmhiBGEkUjwSDBrMbJb+aOsRXVdKCX51CtPamE0fpURtiQk4HSt6p3FiRggfBJCqdrC+ooBIicpooxljh9PLAaIkIRsQe4t5CFyEP5ky0trWT0ei1scRB4x/pJglm2vgDJmsBAHyXOBIeEEWIGNEQc7fMhJyO+LmnRATzRQnTsCUG5dhOK9PClZJ5ANAmW9oJAbOAxNr7Ym/RhWy+lfFn5PWh0Qz0Z3QmuF1CS4iN1lx3A6AVEsK4qBJSIRqdO11khFE8/r5fK8y2j9j/IzBbl1kbfIdcnwEMmamGIJB8AXrYlccfjKHHISYvFq5zBDgjbWZWCoQMBhhATVCcsuR98FmUyQoJwSMCFmnGIZIkEYDTNTHq2GXIZIkOYiCNUqiCjoOFC5kMSQ4kG/54VRV80But3QC/oXDhjS9sZ8+ufWO8mb0zvJ29PmkGx/+Aiboa0N+jTpzfmE74351PV/l9Yow3BWYVkAAAAASUVORK5CYII=",
"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": {
"type": "latest",
"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",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMkAAACgCAMAAACR47ilAAABDlBMVEX///8AAAD///////////////////////////8Ihyv/X2r9/f36+vrz8/MhISHt7e339/f19fXn5+fr6+vv7++Dw5Xp6unx8fE9PT3y8vLB4cro6OhFpWDg8OW2traenp7j4+ORkZEnlkXw9/I2nlMYjjj/cnzNzc2qqqrCwsLb29t0vIf/h4//6+3/r7RktHrIyMj/w8eTy6JWVlb/aXMnlkai0q//9fbV1dVJSUnR6de7u7v/19ovLy//4eOwsLD/m6L/TVqi0rD/ub6kpKSEhISx2b1kZGTPz890dHT/pav/fYZUrG3/zdH/zdD/m6GsrKxtbW3/6er/kZn/eYNVrW08PDz/pazf7+MnlUVxCrBzAAAACHRSTlOAAHDfkI9AMGXS8IIAAAzaSURBVHja3NdBb9MwGAbgFgS8rW3ZsR1baVoJrayXLp1abas0VVO1STswBgcO8P9/CXFaGImzAHFTBO8l50fv99lOr99/8RL/el6+6Pd7/Vf4H/Kq33uDA4YQkDz4C3nd6+FQIdXgmHlxMImDUEqFEFTkX0KPjOkdTOIcImIsSRhjUZSD8nTbTSeSvUNzrpTinGubOA7tsJnuJIJppZK8FK5SmRrFtc41rppuMN1ICghXjGAXKqySMs019iiYcMn0+io+n987Sd4IBbLlcnODXSjjUhpeNNPdlIVLpqPVh5PBLidfXQkR1u+HRR4+Lz5mcBE8lcph8mKCKV1IRvF48HPeuUqA7Q6y52ydxmGkUdpGImzGupCM5qeDau4Zj/A4rObszmFolKa5hQVYOpAUDD/nTNGiEh+zdBihUsVtuxnrQjK9Gg/qM7aG4GxYn+0GANF5L0kkwinhkmns6qjPB6sIPg2fy2wJgCipNIuCagmXlB1+rtye3HmCioXl+2JZSC3hEt9RPbs4x3o2/JXFSsNdLQGUMMn95aAR8pVGVlFkJYpvuQEol0q7zW9BCZdMxk2M03G8f3URZF+aLdsMYMZoFrWYsHDJ6tnBOjlf3U+AH+8uAmC9eVycXTSNGFUpT1pMWICksZCT+WiK7yE5JUq40QS73Cy3s+drsVLpFudxmKS+kPFqglLIfsBMqngiCFw272fP1SKM0qwFpb1kOq9bjPiJUaYUv1rGuPc8o0U1tc0sAJqaFpT2ksm7mjpGqA3ZW1iiNc+7kUYXmGWN5SEDUcVxfCTJ9eUvHVUKpUJEOcc6jUwT6ixn/oRlgDa8ppVOJLenv+HwMITuOCxxmLdcAHj0ern4CFjpjrAjSGLvtPIcXshPnB3GSMlqZ+wOsGlBCbb4kmZIPMVv5wljuSos64W/90UrgnYq8SHvrvFHId+PgCjJLYoA2ayGknIWSPElzZA52sRZcozl5q0GsPApiQmmNEtib0PahewszPJUCmAz8yjcaBa0Ks2S28pkTdAilStTSQtkD2XKEsQYHdGuJNdlyDnCsh8xlUoKfClTbkBTZcMp9ZJJ+UKMEZzigcm4SUV1WS6ynBJwrTRLDgbxH5hSo/KPPFsjSd0TrAPJvAS5Rev422ILyrJE2QI8dFXqJasuIE+/MDKpUu5AUmVFIMWXTE7DR6uZwnNKZVWE5EyQIIovuTwwxN97qStr/wDo0PnyJbF3sQfG33subeUwXrj5+sas+TWlDURR/PXMNGtwwRAENkSdkmS0YvEPFKbpAxRqndpOp9//q9TdrNms60PMsrTnweHR39x77jkkWN0vk2SoBSJ2KIXSE1M511JF7NdOSR6qFWWIHUuhHOpP+c6Btq3pdZLb6kiO4UCF7dvUw2f9fnkjS9PrJDdO3G6idIL+Oy0hr+/Q5aa3JzHtfgJXEq7vC6vopuf93prE7FtDuJI8YIe4uDaGsiOSI+e7pVC6bSr3Sw3FJh4liTmSE7hUYRUfODeHYkdiFq5bOJFC4fvVwtJwyk5Ibhxmoul63otfDsUPLIJekdw6jhJzv/o9XGhDEUFvT3KmQAZwLh713PRfFMl7HvSkZU3ySRuJe4mhdLShLNELGlcWRXLpeCRm1D8NZaS9w38Uh9ia5Mb94TKdQgmWmufb7V7LsyM53keWmPHY1s7XFQiPFAsSfbku4VYKhWdKNejPm6+XIrkxGpdz8aEEPu7M9bIhOd6r39VQ2lTz/BIk6NqRHDXzOy3kwRBdAKjn+StFcg+PNupeimTQbLkYY1HO2DOJn+FZWYIaJK1eEOBOC0fqExuS0+aVKyIolTGd5O3rVRjFguS40ZMhReKzKB9hsooSYLyKUq8Oich5fr0+KJIfze6wIjlq0lQUCVllGEW+mMks98DGtUk6QQfLqlFa/ddJKCoihP/ZsqzYhJSUJH8UyWkDkhkDkE7kdhEySeuRgNfIALg2EgUvNYpoicF+hWOATufTMAaicBpO6TPJWRObKJI0BTBOBQlheZrXJeHhqHWvayB41fKLnEFqPRck8ZwiDikNI9AwliSnWsDbzWSRcqj6JB3awr1arwv4ZbMfDsr3giTyV165Z4yTEAqk4Xi8psB8LUg0w39sQuJHPkhCkeWCxEvqksjvW1dVy3eCriQ5UzmdLbj5pAoSIGXTDQAOxCTJ10aGVyTIkmQ1A0iSED9ZrVhek4RX+8DHZ71EdqXlxT8EoSQDzV+SrOecgDtmDUlyZF+6iCccLG4KhOqStPvVbPyAw/IMD0rf0ijLZhHVSLjiwvobebu0IgzXMvtwv3q8HuGJMyx/kzUYgivN0zTNJxoJY0AWMpApv1ySZFD7dLkgGQHf1Rnmzcs4wysKgJaeZ8XtSmkcUmzmbLvdGiQDOJeZ8h5+V99vmYEiTZfMqiRkHYbzBNtQSJKclCQP2K9EHfbwWO2QI79mXSEUpQySSziXeYZbksSMxvqSJI4ebNclOax2SEHSejPIf0ESEI3kH82kGy1QaLIJ55uf8vNTZk3jEWrO5AUJfyO0Z5LuYnpwEEPo24HQN5FXxedw1pBk7zOJ/1Jztk1pA0Ecf7sd4JCHmthaEsSEIB3nxlgbjEGZVqmdseMDZabf/4t0kwM2ydkxzYVz3BeIvpEfm//u3f8eIsbWJPeM+SGfM4ZZ4YxNQu5FzCz4dMk6qSvopMSM0WTm9ZrEZ5ENYERsnqTHFpkJitaun8q1S6kKWxxsIpkAxoT5APw+0U7IGC/WT3akfqKJhIJIbllkAADmhEAxNYWmWtgZP2d6fLc8yWHJ0QqRcBR7YF+nPjyPUP6FSGwQJDTuapXWye+S7jaRQLhkGJEHsKle10Yhkk47PRZ+wrGwnJMG2oNNyMXRD4zGyq2QRvVOCRL67NnffLvI/KSBfv3XjLki20ToPPV6npezOh9MjCE0vAcTQHGmRST40S3DsPAVRBh8yUwDipjcnfSc8duzzvCwhy+9XCn0LQHkz4nkjEhuypGYQhRzIfy1dO4LtZM9uEt73DSPf1zM3EciCXmuC3grg8IikvMyjgSR0M9+LPmAB/S3l6cnHzMm5DRlp7qz2WwhSC7b9tBrZLWDPtckTosgkV2iUiQG6ns1ZjHAFL0lYMwq5Xd11o1xFocg8Uaj3mWOpD86wnl8lgQOShYvO9Xjw6TyLpPE9BFugvl5UfCSB5k26xcI4tLTNQrluZbZJ5KSbqrcT1hk4gtHhuXqfR+KrWpNM6WL2smjO1uQToQ/SDGM/SmJ5KSk4UVasP2k8PLk/W2E700LCskkc/D5F9ld+dqFfmD2b6bVtswRkSiuOlAYPKCqGwQ2FHRWavCUMlNJ8Nnv37K+j3L9xJIVj5LXvYZN69goEwJx/28k3KYaoLI6px5IgtOsdDe5gnq72apqxfQENAQ9XHW4ym8teHur2PVk0AWnWZkormKD80o7C2gRO9VNlEjG2p2iWlyD8/tWWjikr24HjgN6IpZJU2zwogUt9b1E4GjcqEaCN+BCfrgUScZ6NU8k1VQuIsHHS3tSUCdG6ulyQTR4VRLY15cU2oCTVvwV1NrdN7fLdtNP6jA93Rz+78ZnNNV3Pg+cV0lKDabuCqS2261mD/d7zUmhO1rujo8vAEFUjm0IEjkpXwaw/RCnAhEliZ2Oyvlyzac2ZKXEKEjQamFyuripXpWEkkJxDtuPD/t/nP2TvS7CNBUvKZNPN+lcmR+v/tNuoyGuwFQAyZEMDrZ2LJNC/t7G9R3lyy9zJHDzTkf9kgYVA/VbfImEGj3Vry1NVOSzVGc1QIpqT5V/cnRJ5bDis1REQoe1dOywR7VXXCeJhJ4vHV0F1V7t2EjjSf9/gzhV6FHj7QvSdS7V2lK6bsSQQCrvW8+SDP62dy8pEsJAAIbtoZmpOkA2IdmUceUiIJiFB3Dldu5/lWFgoKBjTyeW1Yj4ncCfxMfGSkDVDWYQNf6U5hLmSS0lD/EgxiWZqdNYf/5HT+FDVXnCUs4H1LkLS6dejRZ2YUdU2rjlk8gM7KBHtZCK6XDy78kpKIbUTewDidagZkjdFMUxwWZ2VAgpLOEnGAsWNrEOH3SCp1Z9CUy0Nm20FncwmqCOsAQ8YcYlKMcdgheUpIT1mBujh0LedJjrW9jb9knJqSDGDw5XdAPUk5fwDnsUeHr1itb2hKuchyryEmbwmRAHm12Yn4fIFfIFkZcw/43/CW6J5lcfo6MOmXBB5CW5RLgDmkFAVsKMuKUzLcjJS8AbOnAHl5TwAx22g0sKpYAbuKTawSVVpkiVy9FbUCAoYTbS0TJAdArVS85YeBfZyWBmCfgELYNtQZu8hLV2NtE5or8ACks0aXpvBJecw1VyPFfJ8Vwlx9M0H3AOTfMJ5/DV3O5wBvfbeU5d/wHsiOrJpqVt0wAAAABJRU5ErkJggg==",
"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": {
"type": "latest",
"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",
"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=",
"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": {
"type": "latest",
"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",
"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=",
"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": {
"type": "timeseries",
"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,
"connectorLogState": "connector_logs"
},
"title": "${entityName} Logs",
"title": "${connectorName} logs",
"showTitleIcon": false,
"dropShadow": true,
"enableFullscreen": true,
@ -1085,13 +1085,14 @@
"showLegend": false,
"actions": {},
"datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false,
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": ""
"noDataDisplayMessage": "",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
},
"row": 0,
"col": 0,
@ -1494,15 +1495,16 @@
"showLegend": false,
"actions": {},
"datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false,
"titleTooltip": "",
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": "",
"borderRadius": "4px"
"borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
},
"row": 0,
"col": 0,
@ -1557,9 +1559,6 @@
},
"title": "Service RPC",
"datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false,
"titleTooltip": "",
"dropShadow": true,
@ -1574,7 +1573,11 @@
"noDataDisplayMessage": "",
"enableDataExport": false,
"showLegend": false,
"borderRadius": "4px"
"borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
},
"row": 0,
"col": 0,
@ -1596,9 +1599,6 @@
},
"title": "Service RPC",
"datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false,
"titleTooltip": "",
"dropShadow": true,
@ -1613,7 +1613,11 @@
"noDataDisplayMessage": "",
"enableDataExport": false,
"showLegend": false,
"borderRadius": "4px"
"borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
},
"row": 0,
"col": 0,
@ -1802,15 +1806,16 @@
"showLegend": false,
"actions": {},
"datasources": [],
"targetDeviceAliasIds": [
"a2f01c66-96cf-49c5-303f-e6f21c559ee8"
],
"showTitleIcon": false,
"titleTooltip": "",
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": "",
"borderRadius": "4px"
"borderRadius": "4px",
"targetDevice": {
"type": "entity",
"entityAliasId": "a2f01c66-96cf-49c5-303f-e6f21c559ee8"
}
},
"row": 0,
"col": 0,
@ -2102,6 +2107,448 @@
"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": {
@ -2134,7 +2581,7 @@
"padding": "0px",
"settings": {
"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,
"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.SwaggerUiConfigProperties;
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.Configuration;
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.exception.ThingsboardCredentialsExpiredResponse;
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.LoginResponse;
@ -79,7 +79,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
@Configuration
@TbCoreComponent
@ConditionalOnExpression("('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && '${springdoc.api-docs.enabled:true}'=='true'")
@Profile("!test")
public class SwaggerConfiguration {
@ -113,7 +113,8 @@ public class SwaggerConfiguration {
private String version;
@Value("${app.version:unknown}")
private String appVersion;
@Value("${swagger.group_name:thingsboard}")
private String groupName;
@Bean
public OpenAPI thingsboardApi() {
@ -212,7 +213,7 @@ public class SwaggerConfiguration {
@Bean
public GroupedOpenApi groupedApi(SpringDocParameterNameDiscoverer localSpringDocParameterNameDiscoverer) {
return GroupedOpenApi.builder()
.group("thingsboard")
.group(groupName)
.pathsToMatch(apiPath)
.addRouterOperationCustomizer(routerOperationCustomizer(localSpringDocParameterNameDiscoverer))
.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 lombok.Getter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -359,29 +360,33 @@ public abstract class BaseController {
private ThingsboardException handleException(Exception exception, boolean logException) {
if (logException && logControllerErrorStackTrace) {
log.error("Error [{}]", exception.getMessage(), exception);
}
String cause = "";
if (exception.getCause() != null) {
cause = exception.getCause().getClass().getCanonicalName();
try {
SecurityUser user = getCurrentUser();
log.error("[{}][{}] Error", user.getTenantId(), user.getId(), exception);
} catch (Exception e) {
log.error("Error", exception);
}
}
Throwable cause = exception.getCause();
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} 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);
} else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL);
} else if (exception instanceof AsyncRequestTimeoutException) {
return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL);
} else if (exception instanceof DataAccessException) {
String errorType = exception.getClass().getSimpleName();
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) {
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;
}
}
if (entity instanceof OAuth2Info oAuth2Info) {
return oAuth2Info.isEdgeEnabled();
}
// Default: If the entity doesn't match any of the conditions, consider it as valid.
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
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
List<EdgeEvent> result = new ArrayList<>();
OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info();
if (!oAuth2Info.isEdgeEnabled()) {
return new PageData<>();
}
List<EdgeEvent> result = new ArrayList<>();
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2,
EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info)));
// 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) {
DownlinkMsg downlinkMsg = null;
OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class);
if (oAuth2Info != null) {
if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) {
OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info);
downlinkMsg = DownlinkMsg.newBuilder()
.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()
.updated(true)
.notificationId(notificationId.getId())
.notificationType(notification.getType())
.newStatus(NotificationStatus.READ)
.build();
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.thingsboard.server.cluster.TbClusterService;
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.DeviceProfile;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
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.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
@ -340,29 +342,33 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, callback);
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
}
@Override
public void onResourceDeleted(TbResourceInfo resource, TbQueueCallback callback) {
log.trace("[{}] Processing delete resource", resource);
TransportProtos.ResourceDeleteMsg resourceUpdateMsg = TransportProtos.ResourceDeleteMsg.newBuilder()
.setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceUpdateMsg).build();
broadcast(transportMsg, callback);
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
log.trace("[{}][{}][{}] Processing delete resource", resource.getTenantId(), resource.getResourceType(), resource.getResourceKey());
TransportProtos.ResourceDeleteMsg resourceDeleteMsg = TransportProtos.ResourceDeleteMsg.newBuilder()
.setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits())
.setResourceType(resource.getResourceType().name())
.setResourceKey(resource.getResourceKey())
.build();
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) {
@ -384,8 +390,19 @@ public class DefaultTbClusterService implements TbClusterService {
}
private void broadcast(ToTransportMsg transportMsg, TbQueueCallback callback) {
TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
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;
for (String transportServiceId : tbTransportServices) {
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 java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@Data
@ -52,4 +51,5 @@ public abstract class TbSubscription<T> {
public int hashCode() {
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.notification.Notification;
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.dao.notification.NotificationService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
@ -78,6 +79,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
.entityId(securityCtx.getId())
.updateProcessor(this::handleNotificationsSubscriptionUpdate)
.limit(cmd.getLimit())
.notificationTypes(cmd.getTypes())
.build();
localSubscriptionService.addSubscription(subscription);
@ -105,8 +107,8 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotifications(NotificationsSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId());
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(),
WEB, (UserId) subscription.getEntityId(), subscription.getLimit());
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientIdAndNotificationTypes(subscription.getTenantId(),
WEB, (UserId) subscription.getEntityId(), subscription.getNotificationTypes(), subscription.getLimit());
subscription.getLatestUnreadNotifications().clear();
notifications.getData().forEach(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);
Notification notification = update.getNotification();
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()) {
subscription.getLatestUnreadNotifications().put(notificationId, notification);
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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsCmdType;
import java.util.Set;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NotificationsSubCmd implements WsCmd {
private int cmdId;
private int limit;
private Set<NotificationType> types;
@Override
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.CmdUpdateType;
import java.util.Collection;
import java.util.List;
@Getter
@ToString(exclude = "notifications")
public class UnreadNotificationsUpdate extends CmdUpdate {
private final Collection<Notification> notifications;
private final List<Notification> notifications;
private final Notification update;
private final int totalUnreadCount;
private final int sequenceNumber;
@ -39,7 +39,7 @@ public class UnreadNotificationsUpdate extends CmdUpdate {
@JsonCreator
public UnreadNotificationsUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
@JsonProperty("errorMsg") String errorMsg,
@JsonProperty("notifications") Collection<Notification> notifications,
@JsonProperty("notifications") List<Notification> notifications,
@JsonProperty("update") Notification update,
@JsonProperty("totalUnreadCount") int totalUnreadCount,
@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 org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import java.util.UUID;
@ -31,6 +32,7 @@ import java.util.UUID;
public class NotificationUpdate {
private UUID notificationId;
private NotificationType notificationType;
private boolean created;
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.ws.notification.cmd.UnreadNotificationsCountUpdate;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@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.Getter;
import org.apache.commons.collections4.CollectionUtils;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
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.TbSubscriptionType;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
@ -29,8 +31,8 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
@ -39,13 +41,19 @@ public class NotificationsSubscription extends AbstractNotificationSubscription<
private final Map<UUID, Notification> latestUnreadNotifications = new HashMap<>();
private final int limit;
private final Set<NotificationType> notificationTypes;
@Builder
public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
BiConsumer<TbSubscription<NotificationsSubscriptionUpdate>, NotificationsSubscriptionUpdate> updateProcessor,
int limit) {
int limit, Set<NotificationType> notificationTypes) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS, updateProcessor);
this.limit = limit;
this.notificationTypes = notificationTypes;
}
public boolean checkNotificationType(NotificationType type) {
return CollectionUtils.isEmpty(notificationTypes) || notificationTypes.contains(type);
}
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}"
# The version of the API doc to display. Default to the package version.
version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
# Queue configuration parameters
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.Primary;
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.Dashboard;
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.StringUtils;
import org.thingsboard.server.common.data.Tenant;
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.edge.Edge;
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.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
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);
}
@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) {
Dashboard dashboard = new Dashboard();
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.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();
tenantProfile.setIsolatedTbRuleEngine(true);
@ -850,4 +858,5 @@ public class TenantControllerTest extends AbstractControllerTest {
testBroadcastEntityStateChangeEventNever(createEntityId_NULL_UUID(new Tenant()));
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 org.junit.Assert;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
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.setEdgeEnabled(false);
doPost("/api/oauth2/config", oAuth2Info, OAuth2Info.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof OAuth2UpdateMsg);
oAuth2UpdateMsg = (OAuth2UpdateMsg) latestMessage;
result = JacksonUtil.fromString(oAuth2UpdateMsg.getEntity(), OAuth2Info.class, true);
Assert.assertEquals(oAuth2Info, result);
Assert.assertFalse(edgeImitator.waitForMessages(5));
edgeImitator.ignoreType(OAuth2UpdateMsg.class);
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);
}
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) {
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);
}
@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
public void testMarkingAsRead_multipleSessions() throws Exception {
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.thingsboard.common.util.JacksonUtil;
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.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
@ -35,7 +36,10 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Slf4j
@Getter
@ -48,18 +52,28 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
private int unreadCount;
private List<Notification> notifications;
private final Map<Integer, UnreadNotificationsUpdate> lastUpdates = new ConcurrentHashMap<>();
public NotificationApiWsClient(String wsUrl) throws URISyntaxException {
super(new URI(wsUrl + "/api/ws"));
}
public NotificationApiWsClient subscribeForUnreadNotifications(int limit) {
send(new NotificationsSubCmd(1, limit));
public NotificationApiWsClient subscribeForUnreadNotifications(int limit, NotificationType... types) {
send(new NotificationsSubCmd(newCmdId(), limit, Arrays.stream(types).collect(Collectors.toSet())));
this.limit = limit;
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() {
send(new NotificationsCountSubCmd(2));
send(new NotificationsCountSubCmd(newCmdId()));
return this;
}
@ -84,6 +98,7 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
CmdUpdateType updateType = CmdUpdateType.valueOf(update.get("cmdUpdateType").asText());
if (updateType == CmdUpdateType.NOTIFICATIONS) {
lastDataUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsUpdate.class);
lastUpdates.put(lastDataUpdate.getCmdId(), lastDataUpdate);
unreadCount = lastDataUpdate.getTotalUnreadCount();
if (lastDataUpdate.getNotifications() != null) {
notifications = new ArrayList<>(lastDataUpdate.getNotifications());
@ -115,7 +130,7 @@ public class NotificationApiWsClient extends TbTestWebSocketClient {
super.onMessage(s);
}
private static int newCmdId() {
private int newCmdId() {
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.UserId;
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.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import java.util.Set;
public interface NotificationService {
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> 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);

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 lombok.Builder;
import lombok.Data;
import lombok.ToString;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -53,9 +54,12 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques
private long startTs;
@Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522")
private long endTs;
@ToString.Exclude
@NoXss
@Schema(description = "JSON object with alarm details")
private JsonNode details;
@Valid
@Schema(description = "JSON object with propagation details")
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 lombok.Builder;
import lombok.Data;
import lombok.ToString;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -47,9 +48,12 @@ public class AlarmUpdateRequest implements AlarmModificationRequest {
private long startTs;
@Schema(description = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522")
private long endTs;
@ToString.Exclude
@NoXss
@Schema(description = "JSON object with alarm details")
private JsonNode details;
@Valid
@Schema(description = "JSON object with propagation details")
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_COMMUNICATION_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.Hashing;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
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.util.AfterStartUp;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -319,7 +319,7 @@ public class HashPartitionService implements PartitionService {
private QueueKey getQueueKey(ServiceType serviceType, String queueName, TenantId tenantId) {
TenantId isolatedOrSystemTenantId = getIsolatedOrSystemTenantId(serviceType, tenantId);
if (queueName == null) {
if (queueName == null || queueName.isEmpty()) {
queueName = MAIN_QUEUE_NAME;
}
QueueKey queueKey = new QueueKey(serviceType, queueName, isolatedOrSystemTenantId);
@ -672,6 +672,7 @@ public class HashPartitionService implements PartitionService {
public QueueConfig(QueueRoutingInfo queueRoutingInfo) {
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.regex.Matcher;
import static java.lang.Character.MAX_RADIX;
import static java.lang.Character.MIN_RADIX;
@Slf4j
public class TbUtils {
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<>();
static {
@ -103,6 +115,8 @@ public class TbUtils {
String.class, int.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class, int.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
String.class)));
parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
@ -175,6 +189,40 @@ public class TbUtils {
float.class, int.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
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",
String.class)));
parserConfig.addImport("base64ToBytes", new MethodStub(TbUtils.class.getMethod("base64ToBytes",
@ -197,6 +245,18 @@ public class TbUtils {
String.class)));
parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI",
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) {
@ -210,6 +270,7 @@ public class TbUtils {
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList));
}
public static Object decodeToJson(ExecutionContext ctx, String jsonStr) throws IOException {
return TbJson.parse(ctx, jsonStr);
}
@ -269,88 +330,127 @@ public class TbUtils {
}
public static Integer parseInt(String value) {
int radix = getRadix(value);
return parseInt(value, radix);
return parseInt(value, ZERO_RADIX);
}
public static Integer parseInt(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try {
String valueP = prepareNumberString(value);
isValidRadix(valueP, radix);
try {
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());
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
return (Integer) compareIntLongValueMinMax(valueP, radixValue, Integer.MAX_VALUE, Integer.MIN_VALUE);
}
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;
}
public static Long parseLong(String value) {
int radix = getRadix(value);
return parseLong(value, radix);
return parseLong(value, ZERO_RADIX);
}
public static Long parseLong(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
try {
String valueP = prepareNumberString(value);
isValidRadix(valueP, radix);
try {
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());
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
return (Long) compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE);
}
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;
}
private static int getRadix(String value, int... radixS) {
return radixS.length > 0 ? radixS[0] : isHexadecimal(value) ? 16 : 10;
private static int parseBinaryStringAsSignedInteger(String binaryString) {
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) {
if (value != null) {
try {
return Float.parseFloat(prepareNumberString(value));
} catch (NumberFormatException e) {
return parseFloat(value, 0);
}
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;
}
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) {
try {
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Double.parseDouble(prepareNumberString(value));
} catch (NumberFormatException e) {
} else {
long bits = Long.parseUnsignedLong(valueP, HEX_RADIX);
return Double.longBitsToDouble(bits);
}
}
return null;
@ -368,9 +468,10 @@ public class TbUtils {
return parseHexToInt(hex, true);
}
public static int parseHexToInt(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8);
return parseBytesToInt(data, 0, data.length, bigEndian);
public static Integer parseHexToInt(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? hexValue : reverseHexStringByOrder(hexValue);
return parseInt(hex, HEX_RADIX);
}
public static long parseLittleEndianHexToLong(String hex) {
@ -385,9 +486,10 @@ public class TbUtils {
return parseHexToLong(hex, true);
}
public static long parseHexToLong(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16);
return parseBytesToLong(data, 0, data.length, bigEndian);
public static Long parseHexToLong(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseLong(hex, HEX_RADIX);
}
public static float parseLittleEndianHexToFloat(String hex) {
@ -402,9 +504,10 @@ public class TbUtils {
return parseHexToFloat(hex, true);
}
public static float parseHexToFloat(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 8);
return parseBytesToFloat(data, 0, bigEndian);
public static Float parseHexToFloat(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseFloat(hex, HEX_RADIX);
}
public static double parseLittleEndianHexToDouble(String hex) {
@ -419,39 +522,171 @@ public class TbUtils {
return parseHexToDouble(hex, true);
}
public static double parseHexToDouble(String hex, boolean bigEndian) {
byte[] data = prepareHexToBytesNumber(hex, 16);
return parseBytesToDouble(data, 0, bigEndian);
public static double parseHexToDouble(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseDouble(hex, HEX_RADIX);
}
private static byte[] prepareHexToBytesNumber(String hex, int len) {
int length = hex.length();
if (length > len) {
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
}
if (length % 2 > 0) {
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String value) {
String hex = prepareNumberString(value);
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
}
byte[] data = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < hex.length(); i += 2) {
// 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;
}
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) {
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
}
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
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)));
public static List<Integer> printUnsignedBytes(ExecutionContext ctx, List<Byte> byteArray) {
ExecutionArrayList<Integer> data = new ExecutionArrayList<>(ctx);
for (Byte b : byteArray) {
// Convert signed byte to unsigned integer
int unsignedByte = Byte.toUnsignedInt(b);
data.add(unsignedByte);
}
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) {
return bytesToHex(Base64.getDecoder().decode(base64));
}
@ -520,8 +755,8 @@ public class TbUtils {
if (offset > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
}
if (length > 8) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!");
if (length > BYTES_LEN_LONG_MAX) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!");
}
if (offset + 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) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, 8, bigEndian);
byte[] bytesToNumber = prepareBytesToNumber(data, offset, BYTES_LEN_LONG_MAX, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getDouble();
}
@ -647,6 +882,15 @@ public class TbUtils {
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) {
if (json instanceof Map.Entry) {
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) {
if (value != null) {
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(",", ".");
}
return value;
}
private static boolean isValidRadix(String value, int radix) {
for (int i = 0; i < value.length(); i++) {
if (i == 0 && value.charAt(i) == '-') {
if (value.length() == 1)
throw new NumberFormatException("Failed radix [" + radix + "] for value: \"" + value + "\"!");
else
continue;
private static int isValidStringAndRadix(String valueP, int radix, String value) {
int radixValue;
if (radix == 0) {
radixValue = getRadix10_16(valueP);
} else if (radix >= 25 && radix <= MAX_RADIX) {
return radix;
} 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) {
if (val > 255 || val.intValue() < -128) {
public static int isBinary(String str) {
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. " +
"Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!");
} else {
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;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserContext;
import org.mvel2.SandboxedParserConfiguration;
@ -31,28 +33,28 @@ import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import static java.lang.Character.MAX_RADIX;
import static java.lang.Character.MIN_RADIX;
@Slf4j
public class TbUtilsTest {
private ExecutionContext ctx;
private final String intValHex = "41EA62CC";
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 long longVal = 0x409B04B10CB295EAL;
private final String longValHex = "409B04B10CB295EA";
private final long longValRev = 0xEA95B20CB1049B40L;
private final String longValHexRev = "EA95B20CB1049B40";
private final String doubleValStr = "1729.1729";
private final double doubleVal = 1729.1729;
private final double doubleValRev = -2.7208640774822924E205;
@ -86,10 +88,9 @@ public class TbUtilsTest {
Assertions.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false));
Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true));
Assertions.assertEquals(0xAABBCC, TbUtils.parseHexToInt("CCBBAA", false));
Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("AABBCCDD", true));
Assertions.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("DDCCBBAA", false));
Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("DDCCBBAA", true));
Assertions.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("AABBCCDD", false));
Assertions.assertThrows(NumberFormatException.class, () -> TbUtils.parseHexToInt("AABBCCDD", true));
Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("11BBCC22", true));
Assertions.assertEquals(0x11BBCC22, TbUtils.parseHexToInt("22CCBB11", false));
}
@Test
@ -208,15 +209,28 @@ public class TbUtilsTest {
Assertions.assertNull(TbUtils.parseInt(""));
Assertions.assertNull(TbUtils.parseInt(" "));
Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("0"));
Assertions.assertEquals(java.util.Optional.of(0).get(), TbUtils.parseInt("-0"));
Assertions.assertEquals((Integer) 0, 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(-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.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.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.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.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.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.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.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
public void parseFloat() {
String floatValStr = "29.29824";
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
@ -246,21 +273,13 @@ public class TbUtilsTest {
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
public void arseBytesToFloat() {
byte[] floatValByte = {65, -22, 98, -52};
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0)));
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(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false)));
}
@ -271,14 +290,15 @@ public class TbUtilsTest {
Assertions.assertNull(TbUtils.parseLong(""));
Assertions.assertNull(TbUtils.parseLong(" "));
Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("0"));
Assertions.assertEquals(java.util.Optional.of(0L).get(), TbUtils.parseLong("-0"));
Assertions.assertEquals((Long) 0L, 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(-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.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.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.assertThrows(NumberFormatException.class, () -> TbUtils.parseLong("KonaLong", 10));
}
@Test
public void parseHexToLong() {
Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHex));
Assertions.assertEquals(longVal, TbUtils.parseHexToLong(longValHexRev, false));
Assertions.assertEquals(longVal, TbUtils.parseBigEndianHexToLong(longValHex));
@ -312,14 +329,20 @@ public class TbUtilsTest {
Bytes.reverse(longValByte);
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));
long longValRev = 0xEA95B20CB1049B40L;
Assertions.assertEquals(longValRev, TbUtils.parseBytesToLong(longVaList, 0, 8));
}
@Test
public void parsDouble() {
String doubleValStr = "1729.1729";
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
@ -329,21 +352,13 @@ public class TbUtilsTest {
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
public void parseBytesToDouble() {
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(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(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
}
@ -354,16 +369,17 @@ public class TbUtilsTest {
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
List<Byte> expectedBytes = TbUtils.stringToBytes(ctx, expectedStr);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assertions.assertEquals(expectedJson,actualJson);
Object actualJson = TbUtils.decodeToJson(ctx, expectedBytes);
Assertions.assertEquals(expectedJson, actualJson);
}
@Test
public void parseStringDecodeToJson() throws IOException {
String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world");
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assertions.assertEquals(expectedJson,actualJson);
Object actualJson = TbUtils.decodeToJson(ctx, expectedStr);
Assertions.assertEquals(expectedJson, actualJson);
}
@Test
@ -386,8 +402,8 @@ public class TbUtilsTest {
@Test
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};
Object[] arrayMix = { "0x00", 8, "16", "0x1C", 255, (byte)0xFC, 173, 136, 117, 116, -118, "-126"};
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"};
String expected = new String(arrayBytes);
ArrayList<Byte> listBytes = new ArrayList<>(arrayBytes.length);
@ -397,12 +413,36 @@ public class TbUtilsTest {
Assertions.assertEquals(expected, TbUtils.bytesToString(listBytes));
ArrayList<Object> listMix = new ArrayList<>(arrayMix.length);
for (Object element : arrayMix) {
listMix.add(element);
}
Collections.addAll(listMix, arrayMix);
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
public void bytesFromList_Error() {
List<String> listHex = new ArrayList<>();
@ -411,14 +451,7 @@ public class TbUtilsTest {
TbUtils.bytesToString(listHex);
Assertions.fail("Should throw NumberFormatException");
} catch (NumberFormatException e) {
Assertions.assertTrue(e.getMessage().contains("Failed radix: [16] for value: \"FG\"!"));
}
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\"!"));
Assertions.assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!"));
}
List<String> listIntString = new ArrayList<>();
@ -482,6 +515,161 @@ public class TbUtilsTest {
String uriDecodeActual = TbUtils.decodeURI(uriEncodeActual);
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) {
List<Byte> result = new ArrayList<>(data.length);
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();
transportResourceCache.evict(tenantId, resourceType, resourceId);
sessions.forEach((id, mdRez) -> {
log.warn("ResourceDelete - [{}] [{}]", id, mdRez);
log.trace("ResourceDelete - [{}] [{}]", id, mdRez);
transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg));
});
} 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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@ -57,6 +56,7 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -235,13 +235,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
publishEvictEvent(new DashboardTitleEvictEvent(dashboardId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(dashboardId).build());
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_default_dashboard_device_profile")) {
throw new DataValidationException("The dashboard referenced by the device profiles cannot be deleted!");
} else {
throw t;
}
} catch (Exception e) {
checkConstraintViolation(e, Map.of(
"fk_default_dashboard_device_profile", "The dashboard is referenced by a device profile",
"fk_default_dashboard_asset_profile", "The dashboard is referenced by an asset profile"
));
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.NotificationDeliveryMethod;
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.PageLink;
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 java.util.Optional;
import java.util.Set;
@Service
@Slf4j
@ -72,10 +74,10 @@ public class DefaultNotificationService implements NotificationService, EntityDa
}
@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);
PageLink pageLink = new PageLink(limit, 0, null, sortOrder);
return findNotificationsByRecipientIdAndReadStatus(tenantId, deliveryMethod, recipientId, true, pageLink);
return notificationDao.findUnreadByDeliveryMethodAndRecipientIdAndNotificationTypesAndPageLink(tenantId, deliveryMethod, recipientId, types, pageLink);
}
@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.NotificationDeliveryMethod;
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.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.Set;
public interface NotificationDao extends Dao<Notification> {
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);
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
public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) {
UUID tenantUUID = request.getTenantId().getId();
log.debug("[{}] createOrUpdateActiveAlarm [{}] {}", tenantUUID, alarmCreationEnabled, request);
AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation());
return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm(
request.getTenantId().getId(),
tenantUUID,
request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID,
request.getEdgeAlarmId() != null ? request.getEdgeAlarmId().getId() : UUID.randomUUID(),
System.currentTimeMillis(),
@ -364,10 +367,14 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override
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());
return toAlarmApiResult(alarmRepository.updateAlarm(
request.getTenantId().getId(),
request.getAlarmId().getId(),
tenantUUID,
alarmUUID,
request.getSeverity().name(),
request.getStartTs(), request.getEndTs(),
getDetailsAsString(request.getDetails()),
@ -380,11 +387,13 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override
public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) {
log.debug("[{}][{}] acknowledgeAlarm [{}]", tenantId, id, ackTs);
return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs));
}
@Override
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));
}

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

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.notification;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.repository.JpaRepository;
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.NotificationDeliveryMethod;
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.PageLink;
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.util.SqlDao;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -58,6 +61,14 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
}
@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) {
return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(),
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.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
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 java.util.Set;
import java.util.UUID;
@Repository
@ -42,7 +44,20 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Param("searchText") String searchText,
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 " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,

2
pom.xml

@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<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>
<paho.client.version>1.2.5</paho.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) {
EntityId originator = msg.getOriginator();
ValueWithTs valueWithTs = cache.get(msg.getOriginator());
return valueWithTs != null ? Futures.immediateFuture(valueWithTs) : fetchLatestValueAsync(ctx, originator);
if (config.isUseCache()) {
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) {

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,
name = "mqtt",
configClazz = TbMqttNodeConfiguration.class,
version = 1,
clusteringMode = ComponentClusteringMode.USER_PREFERENCE,
nodeDescription = "Publish messages to the MQTT broker",
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.ResponseEntity;
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.client.WebClient;
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.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
@ -132,6 +132,7 @@ public class TbHttpClient {
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset")
.build();
} catch (SSLException e) {
throw new TbNodeException(e);
@ -170,7 +171,7 @@ public class TbHttpClient {
BiConsumer<TbMsg, Throwable> onFailure) {
try {
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;
}
@ -183,10 +184,10 @@ public class TbHttpClient {
.uri(uri)
.headers(headers -> prepareHeaders(headers, msg));
if (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) ||
HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method) ||
if ((HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method) ||
HttpMethod.PATCH.equals(method) || HttpMethod.DELETE.equals(method)) &&
!config.isIgnoreRequestBody()) {
request.body(BodyInserters.fromValue(getData(msg, config.isIgnoreRequestBody(), config.isParseToPlainText())));
request.body(BodyInserters.fromValue(getData(msg, config.isParseToPlainText())));
}
request
@ -236,11 +237,9 @@ public class TbHttpClient {
return uri;
}
private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) {
if (!ignoreBody && parseToPlainText) {
return JacksonUtil.toPlainText(tbMsg.getData());
}
return tbMsg.getData();
private Object getData(TbMsg tbMsg, boolean parseToPlainText) {
String data = tbMsg.getData();
return parseToPlainText ? JacksonUtil.toPlainText(data) : JacksonUtil.toJsonNode(data);
}
private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity<String> response) {
@ -283,10 +282,9 @@ public class TbHttpClient {
private TbMsg processException(TbMsg origMsg, Throwable e) {
TbMsgMetaData metaData = origMsg.getMetaData();
metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
if (e instanceof RestClientResponseException) {
RestClientResponseException restClientResponseException = (RestClientResponseException) e;
if (e instanceof WebClientResponseException restClientResponseException) {
metaData.putValue(STATUS, restClientResponseException.getStatusText());
metaData.putValue(STATUS_CODE, restClientResponseException.getRawStatusCode() + "");
metaData.putValue(STATUS_CODE, restClientResponseException.getStatusCode().value() + "");
metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString());
}
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 {
config = new CalculateDeltaNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
}
@Test
@ -166,8 +165,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() {
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
var msgData = "{\"pulseCounter\": 42}";
var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData);
@ -181,8 +181,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() {
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY);
// WHEN
@ -196,8 +197,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
@Test
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() {
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
// WHEN
@ -451,8 +453,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenInvalidStringValue_whenOnMsg_thenException() {
public void givenInvalidStringValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high")));
var msgData = "{\"pulseCounter\":\"123\"}";
@ -475,8 +478,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenBooleanValue_whenOnMsg_thenException() {
public void givenBooleanValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false)));
var msgData = "{\"pulseCounter\":true}";
@ -499,8 +503,9 @@ public class CalculateDeltaNodeTest extends AbstractRuleNodeUpgradeTest {
}
@Test
public void givenJsonValue_whenOnMsg_thenException() {
public void givenJsonValue_whenOnMsg_thenException() throws TbNodeException {
// GIVEN
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}")));
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 {
assertEquals(path, request.getRequestLine().getUri(), "Request path matches");
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");
assertTrue(request.containsHeader("Content-Length"), "Content-Length included");
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);
}
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 {
function s4(): string {
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>
</ng-container>
<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>
</ng-template>
<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({
selector: 'tb-show-notification-popover',
templateUrl: './show-notification-popover.component.html',
styleUrls: []
styleUrls: ['show-notification-popover.component.scss']
})
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 {
LabelValueCardBasicConfigComponent
} 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({
declarations: [
@ -185,7 +188,8 @@ import {
DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent
LabelValueCardBasicConfigComponent,
UnreadNotificationBasicConfigComponent
],
imports: [
CommonModule,
@ -234,7 +238,8 @@ import {
DigitalSimpleGaugeBasicConfigComponent,
MobileAppQrCodeBasicConfigComponent,
LabelCardBasicConfigComponent,
LabelValueCardBasicConfigComponent
LabelValueCardBasicConfigComponent,
UnreadNotificationBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -277,5 +282,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-digital-simple-gauge-basic-config': DigitalSimpleGaugeBasicConfigComponent,
'tb-mobile-app-qr-code-basic-config': MobileAppQrCodeBasicConfigComponent,
'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,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isUndefined, mergeDeep } from '@core/utils';
import { formatValue, isUndefined, mergeDeepIgnoreArray } from '@core/utils';
import {
cssSizeToStrSize,
DateFormatProcessor,
@ -117,7 +117,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: RangeChartWidgetSettings = mergeDeep<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
const settings: RangeChartWidgetSettings = mergeDeepIgnoreArray<RangeChartWidgetSettings>({} as RangeChartWidgetSettings,
rangeChartDefaultSettings, configData.config.settings as RangeChartWidgetSettings);
const iconSize = resolveCssSize(configData.config.iconSize);
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
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import {
TimeSeriesChartTooltipTrigger,
TimeSeriesChartKeySettings,
TimeSeriesChartType,
TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { TimeSeriesChartTooltipTrigger } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
@Component({
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,
TimeSeriesChartSettings,
TimeSeriesChartThreshold,
TimeSeriesChartTooltipWidgetSettings,
TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
@ -48,6 +47,9 @@ import {
ChartFillSettings,
ChartFillType
} 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 {
dataZoom: boolean;

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

@ -38,7 +38,6 @@ import {
TimeSeriesChartSettings,
TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartTooltipWidgetSettings,
TimeSeriesChartVisualMapPiece,
TimeSeriesChartXAxisSettings,
TimeSeriesChartYAxisSettings
@ -54,6 +53,9 @@ import {
ChartLineType,
ChartShape
} from '@home/components/widget/lib/chart/chart.models';
import {
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export interface RangeItem {
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,
TimeSeriesChartStateSourceType,
TimeSeriesChartTicksFormatter,
TimeSeriesChartTicksGenerator,
TimeSeriesChartTooltipValueFormatFunction
TimeSeriesChartTicksGenerator
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { UtilsService } from '@core/services/utils.service';
import { FormattedData } from '@shared/models/widget.models';
import { formatValue, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import { LabelFormatterCallback } from 'echarts';
import {
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
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>
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container>
</th>
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].min }}
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].min | safe: 'html'">
</td>
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].max }}
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].max | safe: 'html'">
</td>
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].avg }}
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].avg | safe: 'html'">
</td>
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].total }}
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].total | safe: 'html'">
</td>
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle">
{{ legendData.data[legendKey.dataIndex].latest }}
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex].latest | safe: 'html'">
</td>
</tr>
</tbody>
@ -106,15 +106,15 @@
(mouseleave)="onLegendKeyLeave(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 [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>
</ng-template>
<ng-template #legendDataRow let-label="label" let-type="type">
<tr>
<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">
{{ legendData.data[legendKey.dataIndex][type] }}
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value" [style]="legendValueStyle"
[innerHTML]="legendData.data[legendKey.dataIndex][type] | safe: 'html'">
</td>
</tr>
</ng-template>

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

@ -23,15 +23,12 @@ import {
autoDateFormat,
AutoDateFormatSettings,
ComponentStyle,
DateFormatProcessor,
DateFormatSettings,
Font,
tsToFormatTimeUnit,
ValueSourceConfig,
ValueSourceType
} from '@shared/models/widget-settings.models';
import {
CallbackDataParams,
TimeAxisBandWidthCalculator,
VisualMapComponentOption,
XAXisOption,
@ -75,12 +72,10 @@ import { BuiltinTextPosition } from 'zrender/src/core/types';
import { CartesianAxisOption } from 'echarts/types/src/coord/cartesian/AxisModel';
import {
calculateAggIntervalWithWidgetTimeWindow,
Interval,
IntervalMath,
WidgetTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { Renderer2 } from '@angular/core';
import {
chartAnimationDefaultSettings,
ChartAnimationSettings,
@ -98,6 +93,11 @@ import {
prepareChartThemeColor
} from '@home/components/widget/lib/chart/chart.models';
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];
@ -127,9 +127,6 @@ const toTimeSeriesChartDataEntry = (entry: DataEntry, valueConverter?: (value: a
return item;
};
export type TimeSeriesChartTooltipValueFormatFunction =
(value: any, latestData: FormattedData, units?: string, decimals?: number) => string;
export interface TimeSeriesChartDataItem {
id: string;
datasource: Datasource;
@ -216,245 +213,6 @@ export const adjustTimeAxisExtentToData = (timeAxisOption: TimeAxisBaseOption,
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 {
default = 'default',

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

@ -21,7 +21,6 @@ import {
createTimeSeriesVisualMapOption,
createTimeSeriesXAxis,
createTimeSeriesYAxis,
createTooltipValueFormatFunction,
defaultTimeSeriesChartYAxisSettings,
generateChartData,
LineSeriesStepType,
@ -37,9 +36,6 @@ import {
TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartThresholdItem,
timeSeriesChartTooltipFormatter,
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction,
TimeSeriesChartType,
TimeSeriesChartXAxis,
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 { 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 {
createTooltipValueFormatFunction,
TimeSeriesChartTooltip,
TimeSeriesChartTooltipTrigger,
TimeSeriesChartTooltipValueFormatFunction
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
export class TbTimeSeriesChart {
@ -133,7 +135,7 @@ export class TbTimeSeriesChart {
private timeSeriesChartOptions: EChartsOption;
private readonly tooltipDateFormat: DateFormatProcessor;
private readonly tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
private readonly timeSeriesChartTooltip: TimeSeriesChartTooltip;
private readonly stateValueConverter: TimeSeriesChartStateValueConverter;
private yMinSubject = new BehaviorSubject(-1);
@ -162,6 +164,8 @@ export class TbTimeSeriesChart {
private renderer: Renderer2,
private autoResize = true) {
let tooltipValueFormatFunction: TimeSeriesChartTooltipValueFormatFunction;
this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings);
@ -169,7 +173,7 @@ export class TbTimeSeriesChart {
this.stackMode = !this.comparisonEnabled && this.settings.stack;
if (this.settings.states && this.settings.states.length) {
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 = $dashboardPageElement.length ? $($dashboardPageElement[$dashboardPageElement.length-1]) : null;
@ -183,14 +187,20 @@ export class TbTimeSeriesChart {
if (this.settings.tooltipShowDate) {
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
}
if (!this.tooltipValueFormatFunction) {
this.tooltipValueFormatFunction =
createTooltipValueFormatFunction(this.settings.tooltipValueFormatter);
if (!this.tooltipValueFormatFunction) {
this.tooltipValueFormatFunction = (value, _latestData, units, decimals) => formatValue(value, decimals, units, false);
if (!tooltipValueFormatFunction) {
tooltipValueFormatFunction = createTooltipValueFormatFunction(this.settings.tooltipValueFormatter);
if (!tooltipValueFormatFunction) {
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();
if (this.autoResize) {
this.shapeResize$ = new ResizeObserver(() => {
@ -603,10 +613,12 @@ export class TbTimeSeriesChart {
type: this.noAggregation ? 'line' : 'shadow'
},
formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? timeSeriesChartTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.tooltipValueFormatFunction,
this.timeSeriesChartTooltip.formatted(
params,
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],
backgroundColor: this.settings.tooltipBackgroundColor,
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">
warning
</mat-icon>
<div matSuffix
<div *ngIf="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
matSuffix
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-style]="{maxWidth: '970px'}">
</div>
@ -83,9 +84,10 @@
class="tb-error">
warning
</mat-icon>
<div matSuffix
<div *ngIf="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
matSuffix
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-style]="{maxWidth: '970px'}">
</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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
NgZone,
OnDestroy,
OnInit,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
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 {
ControlValueAccessor,
FormBuilder,
@ -50,6 +42,7 @@ import {
import {
DeviceInfoType,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
SourceTypes,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@ -88,7 +81,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
required = false;
@Input()
sourceTypes: Array<SourceTypes> = Object.values(SourceTypes);
sourceTypes: Array<SourceTypes | OPCUaSourceTypes> = Object.values(SourceTypes);
deviceInfoTypeValue: any;
@ -111,14 +104,6 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
constructor(protected store: Store<AppState>,
public translate: TranslateService,
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) {
super(store);
}
@ -131,13 +116,13 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
if (this.useSource) {
this.mappingFormGroup.addControl('deviceNameExpressionSource',
this.fb.control(SourceTypes.MSG, []));
this.fb.control(this.sourceTypes[0], []));
}
if (this.deviceInfoType === DeviceInfoType.FULL) {
if (this.useSource) {
this.mappingFormGroup.addControl('deviceProfileExpressionSource',
this.fb.control(SourceTypes.MSG, []));
this.fb.control(this.sourceTypes[0], []));
}
this.mappingFormGroup.addControl('deviceProfileExpression',
this.fb.control('', this.required ?
@ -154,6 +139,7 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
registerOnChange(fn: any): void {
@ -175,5 +161,4 @@ export class DeviceInfoTableComponent extends PageComponent implements ControlVa
updateView(value: any) {
this.propagateChange(value);
}
}

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

Loading…
Cancel
Save