Browse Source

Merge branch 'master' into bugfix/upgrade-rule-node-configurations

pull/9607/head
ShvaykaD 3 years ago
parent
commit
1ecb9497e2
  1. 6
      application/src/main/data/json/system/widget_bundles/charts.json
  2. 31
      application/src/main/data/json/system/widget_types/doughnut.json
  3. 30
      application/src/main/data/json/system/widget_types/doughnut_deprecated.json
  4. 29
      application/src/main/data/json/system/widget_types/horizontal_doughnut.json
  5. 2
      application/src/main/data/json/system/widget_types/vertical_cylinder_tank.json
  6. 22
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  7. 43
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  11. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java
  12. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java
  13. 117
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
  14. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  15. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java
  17. 22
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java
  18. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java
  19. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java
  20. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java
  21. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java
  22. 27
      application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java
  23. 8
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  24. 21
      application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java
  25. 10
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  26. 2
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  27. 1
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  28. 131
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  29. 2
      application/src/main/resources/thingsboard.yml
  30. 276
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  31. 127
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java
  32. 21
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  33. 56
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  34. 41
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  35. 7
      application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
  36. 89
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  37. 4
      application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java
  38. 4
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  39. 51
      application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java
  40. 2
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
  41. 4
      application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java
  42. 13
      common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java
  43. 8
      common/cluster-api/src/main/proto/queue.proto
  44. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  45. 5
      common/edge-api/src/main/proto/edge.proto
  46. 3
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  47. 24
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  48. 9
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
  49. 20
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  50. 3
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  51. 17
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  52. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java
  53. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java
  54. 18
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  55. 33
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  56. 14
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java
  57. 1
      msa/black-box-tests/src/test/resources/logback.xml
  58. 58
      msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml
  59. 57
      msa/black-box-tests/src/test/resources/tb-transports/coap/conf/logback.xml
  60. 57
      msa/black-box-tests/src/test/resources/tb-transports/http/conf/logback.xml
  61. 55
      msa/black-box-tests/src/test/resources/tb-transports/mqtt/conf/logback.xml
  62. 2
      msa/tb/pom.xml
  63. 3
      packaging/java/scripts/control/deb/preinst
  64. 1
      packaging/js/scripts/control/deb/preinst
  65. 17
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  66. 23
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  67. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  68. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  69. 45
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
  70. 23
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
  71. 3
      ui-ngx/package.json
  72. 4
      ui-ngx/src/app/core/api/widget-subscription.ts
  73. 12
      ui-ngx/src/app/core/http/entity.service.ts
  74. 9
      ui-ngx/src/app/core/http/widget.service.ts
  75. 4
      ui-ngx/src/app/core/utils.ts
  76. 3
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss
  77. 8
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  78. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  79. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  80. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  81. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  82. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  83. 86
      ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html
  84. 19
      ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss
  85. 30
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  86. 2
      ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts
  87. 4
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss
  88. 2
      ui-ngx/src/app/modules/home/components/public-api.ts
  89. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html
  90. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  91. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html
  92. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  93. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.html
  94. 254
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html
  95. 341
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts
  96. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  97. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  98. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  99. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.ts
  100. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html

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

@ -16,8 +16,10 @@
"charts.bars", "charts.bars",
"charts.pie", "charts.pie",
"charts.pie_chart_js", "charts.pie_chart_js",
"charts.doughnut_chart_js", "doughnut",
"horizontal_doughnut",
"charts.polar_area_chart_js", "charts.polar_area_chart_js",
"charts.radar_chart_js" "charts.radar_chart_js",
"charts.doughnut_chart_js"
] ]
} }

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -17,7 +17,7 @@
"settingsDirective": "tb-liquid-level-card-widget-settings", "settingsDirective": "tb-liquid-level-card-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-liquid-level-card-basic-config", "basicModeDirective": "tb-liquid-level-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Cylinder\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(255, 255, 255, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.floor(Math.random() * 101);\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"tankSelectionType\":\"static\",\"selectedShape\":\"Vertical Cylinder\",\"shapeAttributeName\":\"tankShape\",\"tankColor\":{\"type\":\"range\",\"color\":\"#242770\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E73535DE\"},{\"from\":20,\"to\":null,\"color\":\"#242770\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E73535DE';\\n }\\n}\\nreturn '#242770';\"},\"datasourceUnits\":\"%\",\"layout\":\"percentage\",\"volumeSource\":\"static\",\"volumeConstant\":500,\"volumeAttributeName\":\"volume\",\"volumeUnits\":\"L\",\"volumeFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"volumeColor\":\"rgba(0, 0, 0, 0.18)\",\"units\":\"%\",\"widgetUnitsSource\":\"static\",\"widgetUnitsAttributeName\":\"units\",\"liquidColor\":{\"type\":\"range\",\"color\":\"#7A8BFF\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#E27C7CDE\"},{\"from\":20,\"to\":null,\"color\":\"#7A8BFF\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"valueColor\":{\"type\":\"range\",\"color\":\"#000000DE\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FF0000DE\"},{\"from\":20,\"to\":null,\"color\":\"rgba(0,0,0,0.87)\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FF0000DE';\\n }\\n}\\nreturn '#000000DE';\"},\"showBackgroundOverlay\":true,\"backgroundOverlayColor\":{\"type\":\"range\",\"color\":\"rgba(255, 255, 255, 0.76)\",\"rangeList\":[{\"from\":null,\"to\":20,\"color\":\"#FFEFEFDE\"},{\"from\":20,\"to\":null,\"color\":\"#FFFFFFC2\"}],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#FFEFEFDE';\\n }\\n}\\nreturn '#FFFFFFC2';\"},\"showTooltip\":true,\"showTooltipLevel\":true,\"tooltipUnits\":\"%\",\"tooltipLevelDecimals\":0,\"tooltipLevelFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipLevelColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.76)\",\"rangeList\":[],\"colorFunction\":\"var percent = value;\\nif (typeof percent !== undefined) {\\n if (percent < 20) {\\n return '#E27C7CDE';\\n }\\n}\\nreturn '#7A8BFF';\"},\"showTooltipDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":true,\"custom\":false},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"100%\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":3,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Liquid level\",\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"configMode\":\"basic\",\"titleFont\":{\"family\":\"Roboto\",\"size\":16,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1.5\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"showTitleIcon\":false,\"titleIcon\":\"water_drop\",\"iconColor\":\"#5469FF\",\"decimals\":0,\"enableDataExport\":false,\"enableFullscreen\":false,\"borderRadius\":\"0px\",\"actions\":{},\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"margin\":\"0px\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}, },
"externalId": null, "externalId": null,
"tags": [ "tags": [

22
application/src/main/java/org/thingsboard/server/controller/AuditLogController.java

@ -99,7 +99,7 @@ public class AuditLogController extends BaseController {
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
checkParameter("CustomerId", strCustomerId); checkParameter("CustomerId", strCustomerId);
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr); List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink)); return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), actionTypes, pageLink));
} }
@ -133,7 +133,7 @@ public class AuditLogController extends BaseController {
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
checkParameter("UserId", strUserId); checkParameter("UserId", strUserId);
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr); List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink)); return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), actionTypes, pageLink));
} }
@ -171,7 +171,7 @@ public class AuditLogController extends BaseController {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr); List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink)); return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), actionTypes, pageLink));
} }
@ -202,7 +202,7 @@ public class AuditLogController extends BaseController {
@RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr); List<ActionType> actionTypes = parseActionTypesStr(actionTypesStr);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, getStartTime(startTime), getEndTime(endTime));
return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink)); return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, actionTypes, pageLink));
} }
@ -214,4 +214,18 @@ public class AuditLogController extends BaseController {
} }
return result; return result;
} }
private Long getStartTime(Long startTime) {
if (startTime == null) {
return 1L;
}
return startTime;
}
private Long getEndTime(Long endTime) {
if (endTime == null) {
return System.currentTimeMillis();
}
return endTime;
}
} }

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

@ -105,6 +105,7 @@ import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARA
import static org.thingsboard.server.controller.ControllerConstants.USER_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.USER_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.dao.entity.BaseEntityService.NULL_CUSTOMER_ID;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@ -439,32 +440,28 @@ public class UserController extends BaseController {
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
try { checkParameter("alarmId", strAlarmId);
checkParameter("alarmId", strAlarmId); AlarmId alarmEntityId = new AlarmId(toUUID(strAlarmId));
AlarmId alarmEntityId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmEntityId, Operation.READ);
Alarm alarm = checkAlarmId(alarmEntityId, Operation.READ); SecurityUser currentUser = getCurrentUser();
SecurityUser currentUser = getCurrentUser(); TenantId tenantId = currentUser.getTenantId();
TenantId tenantId = currentUser.getTenantId(); CustomerId originatorCustomerId = entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).orElse(NULL_CUSTOMER_ID);
CustomerId originatorCustomerId = entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator()).get(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageData<User> pageData;
PageData<User> pageData; if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) {
if (Authority.TENANT_ADMIN.equals(currentUser.getAuthority())) { if (alarm.getCustomerId() == null) {
if (alarm.getCustomerId() == null) { pageData = userService.findTenantAdmins(tenantId, pageLink);
pageData = userService.findTenantAdmins(tenantId, pageLink);
} else {
ArrayList<CustomerId> customerIds = new ArrayList<>(Collections.singletonList(new CustomerId(CustomerId.NULL_UUID)));
if (!CustomerId.NULL_UUID.equals(originatorCustomerId.getId())) {
customerIds.add(originatorCustomerId);
}
pageData = userService.findUsersByCustomerIds(tenantId, customerIds, pageLink);
}
} else { } else {
pageData = userService.findCustomerUsers(tenantId, alarm.getCustomerId(), pageLink); ArrayList<CustomerId> customerIds = new ArrayList<>(Collections.singletonList(NULL_CUSTOMER_ID));
if (!CustomerId.NULL_UUID.equals(originatorCustomerId.getId())) {
customerIds.add(originatorCustomerId);
}
pageData = userService.findUsersByCustomerIds(tenantId, customerIds, pageLink);
} }
return pageData.mapData(user -> new UserEmailInfo(user.getId(), user.getEmail(), user.getFirstName(), user.getLastName())); } else {
} catch (Exception e) { pageData = userService.findCustomerUsers(tenantId, alarm.getCustomerId(), pageLink);
throw handleException(e);
} }
return pageData.mapData(user -> new UserEmailInfo(user.getId(), user.getEmail(), user.getFirstName(), user.getLastName()));
} }
@ApiOperation(value = "Save user settings (saveUserSettings)", @ApiOperation(value = "Save user settings (saveUserSettings)",

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.edge; package org.thingsboard.server.service.edge;
import freemarker.template.Configuration;
import lombok.Data; import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -87,9 +86,6 @@ public class EdgeContextComponent {
@Autowired @Autowired
private AdminSettingsService adminSettingsService; private AdminSettingsService adminSettingsService;
@Autowired
private Configuration freemarkerConfig;
@Autowired @Autowired
private DeviceService deviceService; private DeviceService deviceService;

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

@ -641,7 +641,7 @@ public final class EdgeGrpcSession implements Closeable {
case WIDGETS_BUNDLE: case WIDGETS_BUNDLE:
return ctx.getWidgetBundleProcessor().convertWidgetsBundleEventToDownlink(edgeEvent); return ctx.getWidgetBundleProcessor().convertWidgetsBundleEventToDownlink(edgeEvent);
case WIDGET_TYPE: case WIDGET_TYPE:
return ctx.getWidgetTypeProcessor().convertWidgetTypeEventToDownlink(edgeEvent); return ctx.getWidgetTypeProcessor().convertWidgetTypeEventToDownlink(edgeEvent, this.edgeVersion);
case ADMIN_SETTINGS: case ADMIN_SETTINGS:
return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent); return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent);
case OTA_PACKAGE: case OTA_PACKAGE:
@ -651,9 +651,9 @@ public final class EdgeGrpcSession implements Closeable {
case QUEUE: case QUEUE:
return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent); return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent);
case TENANT: case TENANT:
return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent); return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.getEdgeVersion());
case TENANT_PROFILE: case TENANT_PROFILE:
return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent); return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.getEdgeVersion());
default: default:
log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent); log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent);
return null; return null;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -57,7 +57,7 @@ public class EdgeSyncCursor {
fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService())); fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService()));
fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService()));
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));

8
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/TenantProfileMsgConstructor.java

@ -19,10 +19,12 @@ import com.google.protobuf.ByteString;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
@Component @Component
@TbCoreComponent @TbCoreComponent
@ -31,7 +33,9 @@ public class TenantProfileMsgConstructor {
@Autowired @Autowired
private DataDecodingEncodingService dataDecodingEncodingService; private DataDecodingEncodingService dataDecodingEncodingService;
public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile) { public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion) {
ByteString profileData = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_6_1) ?
ByteString.empty() : ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData()));
TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder() TenantProfileUpdateMsg.Builder builder = TenantProfileUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
.setIdMSB(tenantProfile.getId().getId().getMostSignificantBits()) .setIdMSB(tenantProfile.getId().getId().getMostSignificantBits())
@ -39,7 +43,7 @@ public class TenantProfileMsgConstructor {
.setName(tenantProfile.getName()) .setName(tenantProfile.getName())
.setDefault(tenantProfile.isDefault()) .setDefault(tenantProfile.isDefault())
.setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine()) .setIsolatedRuleChain(tenantProfile.isIsolatedTbRuleEngine())
.setProfileDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile.getProfileData()))); .setProfileDataBytes(profileData);
if (tenantProfile.getDescription() != null) { if (tenantProfile.getDescription() != null) {
builder.setDescription(tenantProfile.getDescription()); builder.setDescription(tenantProfile.getDescription());
} }

11
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java

@ -20,9 +20,11 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
import java.util.Arrays; import java.util.Arrays;
@ -30,7 +32,7 @@ import java.util.Arrays;
@TbCoreComponent @TbCoreComponent
public class WidgetTypeMsgConstructor { public class WidgetTypeMsgConstructor {
public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) { public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) {
WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder() WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder()
.setMsgType(msgType) .setMsgType(msgType)
.setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits()) .setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits())
@ -58,7 +60,12 @@ public class WidgetTypeMsgConstructor {
builder.setImage(widgetTypeDetails.getImage()); builder.setImage(widgetTypeDetails.getImage());
} }
if (widgetTypeDetails.getDescription() != null) { if (widgetTypeDetails.getDescription() != null) {
builder.setDescription(widgetTypeDetails.getDescription()); if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_6_0) &&
widgetTypeDetails.getDescription().length() > 255) {
builder.setDescription(widgetTypeDetails.getDescription().substring(0, 254));
} else {
builder.setDescription(widgetTypeDetails.getDescription());
}
} }
builder.setDeprecated(widgetTypeDetails.isDeprecated()); builder.setDeprecated(widgetTypeDetails.isDeprecated());
if (widgetTypeDetails.getTags() != null) { if (widgetTypeDetails.getTags() != null) {

117
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java

@ -15,146 +15,51 @@
*/ */
package org.thingsboard.server.service.edge.rpc.fetch; package org.thingsboard.server.service.edge.rpc.fetch;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.WordUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@AllArgsConstructor @AllArgsConstructor
@Slf4j @Slf4j
public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher { public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final Configuration freemarkerConfig;
private static final Pattern startPattern = Pattern.compile("<div class=\"content\".*?>");
private static final Pattern endPattern = Pattern.compile("<div class=\"footer\".*?>");
private static final List<String> templatesNames = Arrays.asList(
"account.activated.ftl",
"account.lockout.ftl",
"activation.ftl",
"password.was.reset.ftl",
"reset.password.ftl",
"test.ftl");
// TODO: @voba fix format of next templates
// "state.disabled.ftl",
// "state.enabled.ftl",
// "state.warning.ftl",
@Override @Override
public PageLink getPageLink(int pageSize) { public PageLink getPageLink(int pageSize) {
return null; return null;
} }
@Override public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) throws Exception { List<EdgeEvent> result = fetchAdminSettingsForKeys(tenantId, edge.getId(), List.of("general", "mail", "connectivity", "jwt"));
List<EdgeEvent> result = new ArrayList<>();
AdminSettings systemMailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(systemMailSettings)));
AdminSettings tenantMailSettings = convertToTenantAdminSettings(tenantId, systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(tenantMailSettings)));
AdminSettings systemMailTemplates = loadMailTemplates(tenantId);
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(systemMailTemplates)));
AdminSettings tenantMailTemplates = convertToTenantAdminSettings(tenantId, systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(tenantMailTemplates)));
// return PageData object to be in sync with other fetchers // return PageData object to be in sync with other fetchers
return new PageData<>(result, 1, result.size(), false); return new PageData<>(result, 1, result.size(), false);
} }
private AdminSettings loadMailTemplates(TenantId tenantId) throws Exception { private List<EdgeEvent> fetchAdminSettingsForKeys(TenantId tenantId, EdgeId edgeId, List<String> keys) {
Map<String, Object> mailTemplates = new HashMap<>(); List<EdgeEvent> result = new ArrayList<>();
for (String templatesName : templatesNames) { for (String key : keys) {
Template template = freemarkerConfig.getTemplate(templatesName); AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key);
if (template != null) { if (adminSettings != null) {
String name = validateName(template.getName()); result.add(EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.ADMIN_SETTINGS,
Map<String, String> mailTemplate = getMailTemplateFromFile(template.toString()); EdgeEventActionType.UPDATED, null, JacksonUtil.valueToTree(adminSettings)));
if (mailTemplate != null) {
mailTemplates.put(name, mailTemplate);
} else {
log.error("[{}] Can't load mail template from file {}", tenantId, template.getName());
}
}
}
AdminSettings adminSettings = new AdminSettings();
adminSettings.setId(new AdminSettingsId(Uuids.timeBased()));
adminSettings.setKey("mailTemplates");
adminSettings.setJsonValue(JacksonUtil.convertValue(mailTemplates, JsonNode.class));
return adminSettings;
}
private Map<String, String> getMailTemplateFromFile(String stringTemplate) {
Map<String, String> mailTemplate = new HashMap<>();
Matcher start = startPattern.matcher(stringTemplate);
Matcher end = endPattern.matcher(stringTemplate);
if (start.find() && end.find()) {
String body = StringUtils.substringBetween(stringTemplate, start.group(), end.group()).replaceAll("\t", "");
String subject = StringUtils.substringBetween(body, "<h2>", "</h2>");
mailTemplate.put("subject", subject);
mailTemplate.put("body", body);
} else {
return null;
}
return mailTemplate;
}
private String validateName(String name) throws Exception {
StringBuilder nameBuilder = new StringBuilder();
name = name.replace(".ftl", "");
String[] nameParts = name.split("\\.");
if (nameParts.length >= 1) {
nameBuilder.append(nameParts[0]);
for (int i = 1; i < nameParts.length; i++) {
String word = WordUtils.capitalize(nameParts[i]);
nameBuilder.append(word);
} }
return nameBuilder.toString();
} else {
throw new Exception("Error during filename validation");
} }
} return result;
private AdminSettings convertToTenantAdminSettings(TenantId tenantId, String key, ObjectNode jsonValue) {
AdminSettings tenantMailSettings = new AdminSettings();
tenantMailSettings.setTenantId(tenantId);
jsonValue.put("useSystemMailSettings", true);
tenantMailSettings.setJsonValue(jsonValue);
tenantMailSettings.setKey(key);
return tenantMailSettings;
} }
} }

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -467,9 +467,9 @@ public abstract class BaseEdgeProcessor {
EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType());
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId sourceEdgeId = safeGetEdgeId(edgeNotificationMsg.getSourceEdgeIdMSB(), edgeNotificationMsg.getSourceEdgeIdLSB()); EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
if (type.isAllEdgesRelated()) { if (type.isAllEdgesRelated()) {
return processEntityNotificationForAllEdges(tenantId, type, actionType, entityId, sourceEdgeId); return processEntityNotificationForAllEdges(tenantId, type, actionType, entityId, originatorEdgeId);
} else { } else {
JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()); JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody());
EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()); EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB());
@ -481,19 +481,19 @@ public abstract class BaseEdgeProcessor {
if (edgeId != null) { if (edgeId != null) {
return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
} else { } else {
return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, sourceEdgeId); return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId);
} }
case DELETED: case DELETED:
EdgeEventActionType deleted = EdgeEventActionType.DELETED; EdgeEventActionType deleted = EdgeEventActionType.DELETED;
if (edgeId != null) { if (edgeId != null) {
return saveEdgeEvent(tenantId, edgeId, type, deleted, entityId, body); return saveEdgeEvent(tenantId, edgeId, type, deleted, entityId, body);
} else { } else {
return Futures.transform(Futures.allAsList(processActionForAllEdgesByTenantId(tenantId, type, deleted, entityId, body, sourceEdgeId)), return Futures.transform(Futures.allAsList(processActionForAllEdgesByTenantId(tenantId, type, deleted, entityId, body, originatorEdgeId)),
voids -> null, dbCallbackExecutorService); voids -> null, dbCallbackExecutorService);
} }
case ASSIGNED_TO_EDGE: case ASSIGNED_TO_EDGE:
case UNASSIGNED_FROM_EDGE: case UNASSIGNED_FROM_EDGE:
if (sourceEdgeId == null) { if (originatorEdgeId == null) {
ListenableFuture<Void> future = saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); ListenableFuture<Void> future = saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
return Futures.transformAsync(future, unused -> { return Futures.transformAsync(future, unused -> {
if (type.equals(EdgeEventType.RULE_CHAIN)) { if (type.equals(EdgeEventType.RULE_CHAIN)) {

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java

@ -71,7 +71,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor {
public ListenableFuture<Void> processAlarmNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { public ListenableFuture<Void> processAlarmNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); AlarmId alarmId = new AlarmId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId sourceEdgeId = safeGetEdgeId(edgeNotificationMsg.getSourceEdgeIdMSB(), edgeNotificationMsg.getSourceEdgeIdLSB()); EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
switch (actionType) { switch (actionType) {
case DELETED: case DELETED:
Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class); Alarm deletedAlarm = JacksonUtil.fromString(edgeNotificationMsg.getBody(), Alarm.class);
@ -79,7 +79,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
List<ListenableFuture<Void>> delFutures = pushEventToAllRelatedEdges(tenantId, deletedAlarm.getOriginator(), List<ListenableFuture<Void>> delFutures = pushEventToAllRelatedEdges(tenantId, deletedAlarm.getOriginator(),
alarmId, actionType, JacksonUtil.valueToTree(deletedAlarm), sourceEdgeId); alarmId, actionType, JacksonUtil.valueToTree(deletedAlarm), originatorEdgeId);
return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService); return Futures.transform(Futures.allAsList(delFutures), voids -> null, dbCallbackExecutorService);
default: default:
ListenableFuture<Alarm> alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId); ListenableFuture<Alarm> alarmFuture = alarmService.findAlarmByIdAsync(tenantId, alarmId);
@ -92,7 +92,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
List<ListenableFuture<Void>> futures = pushEventToAllRelatedEdges(tenantId, alarm.getOriginator(), List<ListenableFuture<Void>> futures = pushEventToAllRelatedEdges(tenantId, alarm.getOriginator(),
alarmId, actionType, null, sourceEdgeId); alarmId, actionType, null, originatorEdgeId);
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
}, dbCallbackExecutorService); }, dbCallbackExecutorService);
} }

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java

@ -71,10 +71,12 @@ public class RelationEdgeProcessor extends BaseRelationProcessor {
if (relation == null || (relation.getFrom().getEntityType().equals(EntityType.EDGE) || relation.getTo().getEntityType().equals(EntityType.EDGE))) { if (relation == null || (relation.getFrom().getEntityType().equals(EntityType.EDGE) || relation.getTo().getEntityType().equals(EntityType.EDGE))) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
Set<EdgeId> uniqueEdgeIds = new HashSet<>(); Set<EdgeId> uniqueEdgeIds = new HashSet<>();
uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getTo())); uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getTo()));
uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getFrom())); uniqueEdgeIds.addAll(edgeService.findAllRelatedEdgeIds(tenantId, relation.getFrom()));
uniqueEdgeIds.remove(originatorEdgeId);
if (uniqueEdgeIds.isEmpty()) { if (uniqueEdgeIds.isEmpty()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }

22
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java

@ -18,17 +18,20 @@ package org.thingsboard.server.service.edge.rpc.processor.resource;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j @Slf4j
public abstract class BaseResourceProcessor extends BaseEdgeProcessor { public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
protected void saveOrUpdateTbResource(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) { protected boolean saveOrUpdateTbResource(TenantId tenantId, TbResourceId tbResourceId, ResourceUpdateMsg resourceUpdateMsg) {
boolean resourceKeyUpdated = false;
try { try {
boolean created = false; boolean created = false;
TbResource resource = resourceService.findResourceById(tenantId, tbResourceId); TbResource resource = resourceService.findResourceById(tenantId, tbResourceId);
@ -42,9 +45,21 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
resource.setCreatedTime(Uuids.unixTimestamp(tbResourceId.getId())); resource.setCreatedTime(Uuids.unixTimestamp(tbResourceId.getId()));
created = true; created = true;
} }
String resourceKey = resourceUpdateMsg.getResourceKey();
ResourceType resourceType = ResourceType.valueOf(resourceUpdateMsg.getResourceType());
PageDataIterable<TbResource> resourcesIterable = new PageDataIterable<>(
link -> resourceService.findTenantResourcesByResourceTypeAndPageLink(tenantId, resourceType, link), 1024);
for (TbResource tbResource : resourcesIterable) {
if (tbResource.getResourceKey().equals(resourceUpdateMsg.getResourceKey()) && !tbResourceId.equals(tbResource.getId())) {
resourceKey = StringUtils.randomAlphabetic(15) + "_" + resourceKey;
log.warn("[{}] Resource with resource type {} and key {} already exists. Renaming resource key to {}",
tenantId, resourceType, resourceUpdateMsg.getResourceKey(), resourceKey);
resourceKeyUpdated = true;
}
}
resource.setTitle(resourceUpdateMsg.getTitle()); resource.setTitle(resourceUpdateMsg.getTitle());
resource.setResourceKey(resourceUpdateMsg.getResourceKey()); resource.setResourceKey(resourceKey);
resource.setResourceType(ResourceType.valueOf(resourceUpdateMsg.getResourceType())); resource.setResourceType(resourceType);
resource.setFileName(resourceUpdateMsg.getFileName()); resource.setFileName(resourceUpdateMsg.getFileName());
resource.setData(resourceUpdateMsg.hasData() ? resourceUpdateMsg.getData() : null); resource.setData(resourceUpdateMsg.hasData() ? resourceUpdateMsg.getData() : null);
resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null); resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null);
@ -57,5 +72,6 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
log.error("[{}] Failed to process resource update msg [{}]", tenantId, resourceUpdateMsg, e); log.error("[{}] Failed to process resource update msg [{}]", tenantId, resourceUpdateMsg, e);
throw e; throw e;
} }
return resourceKeyUpdated;
} }
} }

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java

@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -46,14 +48,12 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor {
switch (resourceUpdateMsg.getMsgType()) { switch (resourceUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE:
super.saveOrUpdateTbResource(tenantId, tbResourceId, resourceUpdateMsg); boolean resourceKeyUpdated = super.saveOrUpdateTbResource(tenantId, tbResourceId, resourceUpdateMsg);
break; if (resourceKeyUpdated) {
case ENTITY_DELETED_RPC_MESSAGE: saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.TB_RESOURCE, EdgeEventActionType.UPDATED, tbResourceId, null);
TbResource tbResourceToDelete = resourceService.findResourceById(tenantId, tbResourceId);
if (tbResourceToDelete != null) {
resourceService.deleteResource(tenantId, tbResourceId);
} }
break; break;
case ENTITY_DELETED_RPC_MESSAGE:
case UNRECOGNIZED: case UNRECOGNIZED:
return handleUnsupportedMsgType(resourceUpdateMsg.getMsgType()); return handleUnsupportedMsgType(resourceUpdateMsg.getMsgType());
} }

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@TbCoreComponent @TbCoreComponent
public class TenantEdgeProcessor extends BaseEdgeProcessor { public class TenantEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TenantId tenantId = new TenantId(edgeEvent.getEntityId()); TenantId tenantId = new TenantId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
@ -44,7 +45,7 @@ public class TenantEdgeProcessor extends BaseEdgeProcessor {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
TenantUpdateMsg tenantUpdateMsg = tenantMsgConstructor.constructTenantUpdateMsg(msgType, tenant); TenantUpdateMsg tenantUpdateMsg = tenantMsgConstructor.constructTenantUpdateMsg(msgType, tenant);
TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId());
TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile); TenantProfileUpdateMsg tenantProfileUpdateMsg = tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion);
downlinkMsg = DownlinkMsg.newBuilder() downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addTenantUpdateMsg(tenantUpdateMsg) .addTenantUpdateMsg(tenantUpdateMsg)

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -33,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@TbCoreComponent @TbCoreComponent
public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { public class TenantProfileEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertTenantProfileEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertTenantProfileEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId()); TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) {
@ -41,7 +42,7 @@ public class TenantProfileEdgeProcessor extends BaseEdgeProcessor {
if (tenantProfile != null) { if (tenantProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
TenantProfileUpdateMsg tenantProfileUpdateMsg = TenantProfileUpdateMsg tenantProfileUpdateMsg =
tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile); tenantProfileMsgConstructor.constructTenantProfileUpdateMsg(msgType, tenantProfile, edgeVersion);
downlinkMsg = DownlinkMsg.newBuilder() downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addTenantProfileUpdateMsg(tenantProfileUpdateMsg) .addTenantProfileUpdateMsg(tenantProfileUpdateMsg)

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -32,7 +33,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@TbCoreComponent @TbCoreComponent
public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertWidgetTypeEventToDownlink(EdgeEvent edgeEvent) { public DownlinkMsg convertWidgetTypeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -42,7 +43,7 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor {
if (widgetTypeDetails != null) { if (widgetTypeDetails != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
WidgetTypeUpdateMsg widgetTypeUpdateMsg = WidgetTypeUpdateMsg widgetTypeUpdateMsg =
widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails); widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails, edgeVersion);
downlinkMsg = DownlinkMsg.newBuilder() downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg)

27
application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.utils;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@Slf4j
public final class EdgeVersionUtils {
public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) {
return currentVersion.ordinal() < requiredVersion.ordinal();
}
}

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

@ -486,7 +486,7 @@ public class DefaultTbClusterService implements TbClusterService {
} }
@Override @Override
public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId) { public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId originatorEdgeId) {
if (!edgesEnabled) { if (!edgesEnabled) {
return; return;
} }
@ -519,9 +519,9 @@ public class DefaultTbClusterService implements TbClusterService {
if (body != null) { if (body != null) {
builder.setBody(body); builder.setBody(body);
} }
if (sourceEdgeId != null) { if (originatorEdgeId != null) {
builder.setSourceEdgeIdMSB(sourceEdgeId.getId().getMostSignificantBits()); builder.setOriginatorEdgeIdMSB(originatorEdgeId.getId().getMostSignificantBits());
builder.setSourceEdgeIdLSB(sourceEdgeId.getId().getLeastSignificantBits()); builder.setOriginatorEdgeIdLSB(originatorEdgeId.getId().getLeastSignificantBits());
} }
TransportProtos.EdgeNotificationMsgProto msg = builder.build(); TransportProtos.EdgeNotificationMsgProto msg = builder.build();
log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg); log.trace("[{}] sending notification to edge service {}", tenantId.getId(), msg);

21
application/src/main/java/org/thingsboard/server/service/queue/ProtoUtils.java

@ -152,22 +152,29 @@ public class ProtoUtils {
} }
private static TransportProtos.DeviceEdgeUpdateMsgProto toProto(DeviceEdgeUpdateMsg msg) { private static TransportProtos.DeviceEdgeUpdateMsgProto toProto(DeviceEdgeUpdateMsg msg) {
return TransportProtos.DeviceEdgeUpdateMsgProto.newBuilder() TransportProtos.DeviceEdgeUpdateMsgProto.Builder builder = TransportProtos.DeviceEdgeUpdateMsgProto.newBuilder()
.setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(msg.getTenantId().getId().getLeastSignificantBits()) .setTenantIdLSB(msg.getTenantId().getId().getLeastSignificantBits())
.setDeviceIdMSB(msg.getDeviceId().getId().getMostSignificantBits()) .setDeviceIdMSB(msg.getDeviceId().getId().getMostSignificantBits())
.setDeviceIdLSB(msg.getDeviceId().getId().getLeastSignificantBits()) .setDeviceIdLSB(msg.getDeviceId().getId().getLeastSignificantBits());
.setEdgeIdMSB(msg.getEdgeId().getId().getMostSignificantBits())
.setEdgeIdLSB(msg.getEdgeId().getId().getLeastSignificantBits()) if (msg.getEdgeId() != null) {
.build(); builder.setEdgeIdMSB(msg.getEdgeId().getId().getMostSignificantBits())
.setEdgeIdLSB(msg.getEdgeId().getId().getLeastSignificantBits());
}
return builder.build();
} }
private static DeviceEdgeUpdateMsg fromProto(TransportProtos.DeviceEdgeUpdateMsgProto proto) { private static DeviceEdgeUpdateMsg fromProto(TransportProtos.DeviceEdgeUpdateMsgProto proto) {
EdgeId edgeId = null;
if (proto.hasEdgeIdMSB() && proto.hasEdgeIdLSB()) {
edgeId = new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB()));
}
return new DeviceEdgeUpdateMsg( return new DeviceEdgeUpdateMsg(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
new DeviceId(new UUID(proto.getDeviceIdMSB(), proto.getDeviceIdLSB())), new DeviceId(new UUID(proto.getDeviceIdMSB(), proto.getDeviceIdLSB())),
new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())) edgeId);
);
} }
private static TransportProtos.DeviceNameOrTypeUpdateMsgProto toProto(DeviceNameOrTypeUpdateMsg msg) { private static TransportProtos.DeviceNameOrTypeUpdateMsgProto toProto(DeviceNameOrTypeUpdateMsg msg) {

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

@ -59,7 +59,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -129,12 +128,15 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
if (msgs.isEmpty()) { if (msgs.isEmpty()) {
continue; continue;
} }
ConcurrentMap<UUID, TbProtoQueueMsg<N>> pendingMap = msgs.stream().collect( List<IdMsgPair<N>> orderedMsgList = msgs.stream().map(msg -> new IdMsgPair<>(UUID.randomUUID(), msg)).collect(Collectors.toList());
Collectors.toConcurrentMap(s -> UUID.randomUUID(), Function.identity())); ConcurrentMap<UUID, TbProtoQueueMsg<N>> pendingMap = orderedMsgList.stream().collect(
Collectors.toConcurrentMap(IdMsgPair::getUuid, IdMsgPair::getMsg));
CountDownLatch processingTimeoutLatch = new CountDownLatch(1); CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
TbPackProcessingContext<TbProtoQueueMsg<N>> ctx = new TbPackProcessingContext<>( TbPackProcessingContext<TbProtoQueueMsg<N>> ctx = new TbPackProcessingContext<>(
processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
pendingMap.forEach((id, msg) -> { orderedMsgList.forEach(element -> {
UUID id = element.getUuid();
TbProtoQueueMsg<N> msg = element.getMsg();
log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue()); log.trace("[{}] Creating notification callback for message: {}", id, msg.getValue());
TbCallback callback = new TbPackCallback<>(id, ctx); TbCallback callback = new TbPackCallback<>(id, ctx);
try { try {

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

@ -301,7 +301,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
if (subInfo != null) { if (subInfo != null) {
log.trace("[{}][{}] Handling alarm update {}: {}", tenantId, entityId, alarm, deleted); log.trace("[{}][{}] Handling alarm update {}: {}", tenantId, entityId, alarm, deleted);
for (Map.Entry<String, TbSubscriptionsInfo> entry : subInfo.getSubs().entrySet()) { for (Map.Entry<String, TbSubscriptionsInfo> entry : subInfo.getSubs().entrySet()) {
if (entry.getValue().notifications) { if (entry.getValue().alarms) {
onAlarmSubUpdate(entry.getKey(), entityId, alarm, deleted); onAlarmSubUpdate(entry.getKey(), entityId, alarm, deleted);
} }
} }

1
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -173,6 +173,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
} }
public void onSubEventCallback(UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) { public void onSubEventCallback(UUID entityId, int seqNumber, TbEntityUpdatesInfo entityUpdatesInfo, TbCallback callback) {
log.debug("[{}][{}] Processing sub event callback: {}.", entityId, seqNumber, entityUpdatesInfo);
entityUpdates.put(entityId, entityUpdatesInfo); entityUpdates.put(entityId, entityUpdatesInfo);
Set<TbSubscription<?>> pendingSubs = null; Set<TbSubscription<?>> pendingSubs = null;
subsLock.lock(); subsLock.lock();

131
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -192,7 +192,7 @@ public class DefaultTransportApiService implements TransportApiService {
final String certChain = msg.getCertificateChain(); final String certChain = msg.getCertificateChain();
result = handlerExecutor.submit(() -> validateOrCreateDeviceX509Certificate(certChain)); result = handlerExecutor.submit(() -> validateOrCreateDeviceX509Certificate(certChain));
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) { } else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
result = handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()); result = handlerExecutor.submit(() -> handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()));
} else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) {
result = handle(transportApiRequestMsg.getEntityProfileRequestMsg()); result = handle(transportApiRequestMsg.getEntityProfileRequestMsg());
} else if (transportApiRequestMsg.hasLwM2MRequestMsg()) { } else if (transportApiRequestMsg.hasLwM2MRequestMsg()) {
@ -223,7 +223,6 @@ public class DefaultTransportApiService implements TransportApiService {
} }
private TransportApiResponseMsg validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) { private TransportApiResponseMsg validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) {
//TODO: Make async and enable caching
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId); DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId);
if (credentials != null && credentials.getCredentialsType() == credentialsType) { if (credentials != null && credentials.getCredentialsType() == credentialsType) {
return getDeviceInfo(credentials); return getDeviceInfo(credentials);
@ -336,76 +335,74 @@ public class DefaultTransportApiService implements TransportApiService {
return VALID; return VALID;
} }
private ListenableFuture<TransportApiResponseMsg> handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { private TransportApiResponseMsg handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) {
DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB())); DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB()));
ListenableFuture<Device> gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId); Device gateway = deviceService.findDeviceById(TenantId.SYS_TENANT_ID, gatewayId);
return Futures.transform(gatewayFuture, gateway -> { Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(requestMsg.getDeviceName(), id -> new ReentrantLock());
Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(requestMsg.getDeviceName(), id -> new ReentrantLock()); deviceCreationLock.lock();
deviceCreationLock.lock(); try {
try { Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), requestMsg.getDeviceName());
Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), requestMsg.getDeviceName()); if (device == null) {
if (device == null) { TenantId tenantId = gateway.getTenantId();
TenantId tenantId = gateway.getTenantId(); device = new Device();
device = new Device(); device.setTenantId(tenantId);
device.setTenantId(tenantId); device.setName(requestMsg.getDeviceName());
device.setName(requestMsg.getDeviceName()); device.setType(requestMsg.getDeviceType());
device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId());
device.setCustomerId(gateway.getCustomerId()); DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
device.setDeviceProfileId(deviceProfile.getId());
device.setDeviceProfileId(deviceProfile.getId()); ObjectNode additionalInfo = JacksonUtil.newObjectNode();
ObjectNode additionalInfo = JacksonUtil.newObjectNode(); additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString());
additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); device.setAdditionalInfo(additionalInfo);
device.setAdditionalInfo(additionalInfo); Device savedDevice = deviceService.saveDevice(device);
Device savedDevice = deviceService.saveDevice(device); tbClusterService.onDeviceUpdated(savedDevice, null);
tbClusterService.onDeviceUpdated(savedDevice, null); device = savedDevice;
device = savedDevice;
relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created"));
relationService.saveRelation(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created"));
TbMsgMetaData metaData = new TbMsgMetaData();
TbMsgMetaData metaData = new TbMsgMetaData(); CustomerId customerId = gateway.getCustomerId();
CustomerId customerId = gateway.getCustomerId(); if (customerId != null && !customerId.isNullUid()) {
if (customerId != null && !customerId.isNullUid()) { metaData.putValue("customerId", customerId.toString());
metaData.putValue("customerId", customerId.toString()); }
} metaData.putValue("gatewayId", gatewayId.toString());
metaData.putValue("gatewayId", gatewayId.toString());
DeviceId deviceId = device.getId(); DeviceId deviceId = device.getId();
JsonNode entityNode = JacksonUtil.valueToTree(device); JsonNode entityNode = JacksonUtil.valueToTree(device);
TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null);
} else { } else {
JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); JsonNode deviceAdditionalInfo = device.getAdditionalInfo();
if (deviceAdditionalInfo == null) { if (deviceAdditionalInfo == null) {
deviceAdditionalInfo = JacksonUtil.newObjectNode(); deviceAdditionalInfo = JacksonUtil.newObjectNode();
}
if (deviceAdditionalInfo.isObject() &&
(!deviceAdditionalInfo.has(DataConstants.LAST_CONNECTED_GATEWAY)
|| !gatewayId.toString().equals(deviceAdditionalInfo.get(DataConstants.LAST_CONNECTED_GATEWAY).asText()))) {
ObjectNode newDeviceAdditionalInfo = (ObjectNode) deviceAdditionalInfo;
newDeviceAdditionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString());
Device savedDevice = deviceService.saveDevice(device);
tbClusterService.onDeviceUpdated(savedDevice, device);
}
} }
GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() if (deviceAdditionalInfo.isObject() &&
.setDeviceInfo(getDeviceInfoProto(device)); (!deviceAdditionalInfo.has(DataConstants.LAST_CONNECTED_GATEWAY)
DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); || !gatewayId.toString().equals(deviceAdditionalInfo.get(DataConstants.LAST_CONNECTED_GATEWAY).asText()))) {
if (deviceProfile != null) { ObjectNode newDeviceAdditionalInfo = (ObjectNode) deviceAdditionalInfo;
builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); newDeviceAdditionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString());
} else { Device savedDevice = deviceService.saveDevice(device);
log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId()); tbClusterService.onDeviceUpdated(savedDevice, device);
} }
return TransportApiResponseMsg.newBuilder()
.setGetOrCreateDeviceResponseMsg(builder.build())
.build();
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e);
throw new RuntimeException(e);
} finally {
deviceCreationLock.unlock();
} }
}, dbCallbackExecutorService); GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder()
.setDeviceInfo(getDeviceInfoProto(device));
DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId());
if (deviceProfile != null) {
builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
} else {
log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId());
}
return TransportApiResponseMsg.newBuilder()
.setGetOrCreateDeviceResponseMsg(builder.build())
.build();
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e);
throw new RuntimeException(e);
} finally {
deviceCreationLock.unlock();
}
} }
private ListenableFuture<TransportApiResponseMsg> handle(ProvisionDeviceRequestMsg requestMsg) { private ListenableFuture<TransportApiResponseMsg> handle(ProvisionDeviceRequestMsg requestMsg) {

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

@ -191,7 +191,7 @@ ui:
# Help parameters # Help parameters
help: help:
# Base URL for UI help assets # Base URL for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6}" base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.1}"
# Database telemetry parameters # Database telemetry parameters
database: database:

276
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -24,7 +24,6 @@ import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.BeforeAll;
import org.mockito.AdditionalAnswers; import org.mockito.AdditionalAnswers;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -100,8 +99,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"C3swkUdrR3pezg==\n" + "C3swkUdrR3pezg==\n" +
"-----END PRIVATE KEY-----\n"; "-----END PRIVATE KEY-----\n";
ListeningExecutorService executor;
private Tenant savedTenant; private Tenant savedTenant;
private User tenantAdmin; private User tenantAdmin;
private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId mqttDeviceProfileId;
@ -117,22 +114,49 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin(); loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", "");
http.put("port", 8080);
config.set("http", http);
ObjectNode https = JacksonUtil.newObjectNode();
https.put("enabled", true);
https.put("host", "");
https.put("port", 444);
config.set("https", https);
ObjectNode mqtt = JacksonUtil.newObjectNode();
mqtt.put("enabled", true);
mqtt.put("host", "");
mqtt.put("port", 1883);
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", true);
mqtts.put("host", "");
mqtts.put("port", 8883);
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", true);
coap.put("host", "");
coap.put("port", 5683);
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", true);
coaps.put("host", "");
coaps.put("port", 5684);
config.set("coaps", coaps);
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class); AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
JsonNode connectivity = adminSettings.getJsonValue(); adminSettings.setJsonValue(config);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
((ObjectNode)connectivity.get("http")).put("port", 8080);
((ObjectNode)connectivity.get("http")).put("enabled", true);
((ObjectNode)connectivity.get("https")).put("enabled", true);
((ObjectNode)connectivity.get("https")).put("port", 444);
((ObjectNode)connectivity.get("mqtt")).put("enabled", true);
((ObjectNode)connectivity.get("mqtts")).put("enabled", true);
((ObjectNode)connectivity.get("coap")).put("enabled", true);
((ObjectNode)connectivity.get("coaps")).put("enabled", true);
doPost("/api/admin/settings", adminSettings);
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
tenant.setTitle("My tenant"); tenant.setTitle("My tenant");
@ -179,8 +203,6 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
@After @After
public void afterTest() throws Exception { public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin(); loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId()) doDelete("/api/tenant/" + savedTenant.getId().getId())
@ -212,26 +234,32 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " +
"-u %s -m \"{temperature:25}\"", "-u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " +
"-t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", credentials.getCredentialsId())); "-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", " -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP); JsonNode linuxCoapCommands = commands.get(COAP);
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry " + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry " +
"-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); "-t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry" + assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry" +
" -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); " -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER);
assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it --network=host" +
" thingsboard/coap-clients coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it --network=host" +
" thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
} }
@Test @Test
@ -251,18 +279,18 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); "-u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " +
"-t %s -u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); "-t %s -u \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -u %s -m \"{temperature:25}\"", " -p 1883 -t %s -u \"%s\" -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -u %s -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -u \"%s\" -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId()));
} }
@ -295,18 +323,18 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
JsonNode mqttCommands = commands.get(MQTT); JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " +
"-i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); "-i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 " +
"-t %s -i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); "-t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" +
" -p 1883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"", " -p 1883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it --network=host thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + "/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"\"", "mosquitto_pub -d -q 1 --cafile ca-root.pem -h localhost -p 8883 -t %s -i \"%s\" -u \"%s\" -P \"%s\" -m \"{temperature:25}\"\"",
DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); DEVICE_TELEMETRY_TOPIC, clientId, userName, password));
} }
@ -349,9 +377,9 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(commands).hasSize(1); assertThat(commands).hasSize(1);
JsonNode linuxCommands = commands.get(COAP); JsonNode linuxCommands = commands.get(COAP);
assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"",
credentials.getCredentialsId())); credentials.getCredentialsId()));
} }
@ -416,4 +444,172 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
public void testDownloadCertWithUnknownProtocol() throws Exception { public void testDownloadCertWithUnknownProtocol() throws Exception {
doGet("/api/device-connectivity/unknownProtocol/certificate/download").andExpect(status().isNotFound()); doGet("/api/device-connectivity/unknownProtocol/certificate/download").andExpect(status().isNotFound());
} }
@Test
public void testFetchPublishTelemetryCommandsForDefaultDeviceIfPortsSetToDefault() throws Exception {
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", "");
http.put("port", 80);
config.set("http", http);
ObjectNode https = JacksonUtil.newObjectNode();
https.put("enabled", true);
https.put("host", "");
https.put("port", 443);
config.set("https", https);
ObjectNode mqtt = JacksonUtil.newObjectNode();
mqtt.put("enabled", false);
mqtt.put("host", "");
mqtt.put("port", 1883);
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", false);
mqtts.put("host", "");
mqtts.put("port", 8883);
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", false);
coap.put("host", "");
coap.put("port", 5683);
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", false);
coaps.put("host", "");
coaps.put("port", 5684);
config.set("coaps", coaps);
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
adminSettings.setJsonValue(config);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
login("tenant2@thingsboard.org", "testPassword1");
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(1);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDeviceIfHostIsNotLocalhost() throws Exception {
loginSysAdmin();
ObjectNode config = JacksonUtil.newObjectNode();
ObjectNode http = JacksonUtil.newObjectNode();
http.put("enabled", true);
http.put("host", "test.domain");
http.put("port", 8080);
config.set("http", http);
ObjectNode https = JacksonUtil.newObjectNode();
https.put("enabled", true);
https.put("host", "test.domain");
https.put("port", 443);
config.set("https", https);
ObjectNode mqtt = JacksonUtil.newObjectNode();
mqtt.put("enabled", true);
mqtt.put("host", "test.domain");
mqtt.put("port", 1883);
config.set("mqtt", mqtt);
ObjectNode mqtts = JacksonUtil.newObjectNode();
mqtts.put("enabled", true);
mqtts.put("host", "test.domain");
mqtts.put("port", 8883);
config.set("mqtts", mqtts);
ObjectNode coap = JacksonUtil.newObjectNode();
coap.put("enabled", true);
coap.put("host", "test.domain");
coap.put("port", 5683);
config.set("coap", coap);
ObjectNode coaps = JacksonUtil.newObjectNode();
coaps.put("enabled", true);
coaps.put("host", "test.domain");
coaps.put("port", 5684);
config.set("coaps", coaps);
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
adminSettings.setJsonValue(config);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
login("tenant2@thingsboard.org", "testPassword1");
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(3);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://test.domain:8080/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://test.domain/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
JsonNode mqttCommands = commands.get(MQTT);
assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h test.domain -p 1883 -t v1/devices/me/telemetry " +
"-u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download");
assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 " +
"-t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER);
assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h test.domain" +
" -p 1883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " +
"/bin/sh -c \"curl -f -S -o ca-root.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " +
"mosquitto_pub -d -q 1 --cafile ca-root.pem -h test.domain -p 8883 -t v1/devices/me/telemetry -u \"%s\" -m \"{temperature:25}\"\"",
credentials.getCredentialsId()));
JsonNode linuxCoapCommands = commands.get(COAP);
assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -v 6 -m POST coap://test.domain:5683/api/v1/%s/telemetry " +
"-t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry" +
" -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER);
assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " +
"thingsboard/coap-clients coap-client -v 6 -m POST coap://test.domain:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
assertThat(dockerCoapCommands.get(COAPS).asText()).isEqualTo(String.format("docker run --rm -it " +
"thingsboard/coap-clients coap-client-openssl -v 6 -m POST coaps://test.domain:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", credentials.getCredentialsId()));
}
} }

127
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java

@ -1,127 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
@ContextConfiguration(classes = {DeviceConnectivityControllerWithDefaultPortTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractControllerTest {
ListeningExecutorService executor;
private Tenant savedTenant;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class);
JsonNode connectivity = adminSettings.getJsonValue();
((ObjectNode) connectivity.get("http")).put("port", 80);
((ObjectNode) connectivity.get("https")).put("enabled", true);
((ObjectNode) connectivity.get("mqtt")).put("enabled", false);
((ObjectNode) connectivity.get("mqtts")).put("enabled", false);
((ObjectNode) connectivity.get("coaps")).put("enabled", false);
((ObjectNode) connectivity.get("coap")).put("enabled", false);
doPost("/api/admin/settings", adminSettings);
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(1);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
}
}

21
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.edge.EdgeDao;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
@ -843,6 +844,13 @@ public class EdgeControllerTest extends AbstractControllerTest {
@Test @Test
public void testSyncEdge() throws Exception { public void testSyncEdge() throws Exception {
loginSysAdmin();
// get jwt settings from yaml config
JwtSettings settings = doGet("/api/admin/jwtSettings", JwtSettings.class);
// save jwt settings into db
doPost("/api/admin/jwtSettings", settings).andExpect(status().isOk());
loginTenantAdmin();
Asset asset = new Asset(); Asset asset = new Asset();
asset.setName("Test Sync Edge Asset 1"); asset.setName("Test Sync Edge Asset 1");
asset.setType("test"); asset.setType("test");
@ -904,10 +912,10 @@ public class EdgeControllerTest extends AbstractControllerTest {
private void verifyFetchersMsgs(EdgeImitator edgeImitator) { private void verifyFetchersMsgs(EdgeImitator edgeImitator) {
Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main")); Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main"));
Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain")); Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail", true)); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "general"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail", false)); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mailTemplates", true)); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mailTemplates", false)); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "jwt"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
@ -953,12 +961,11 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false; return false;
} }
private boolean popAdminSettingsMsg(List<AbstractMessage> messages, String key, boolean isSystem) { private boolean popAdminSettingsMsg(List<AbstractMessage> messages, String key) {
for (AbstractMessage message : messages) { for (AbstractMessage message : messages) {
if (message instanceof AdminSettingsUpdateMsg) { if (message instanceof AdminSettingsUpdateMsg) {
AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) message; AdminSettingsUpdateMsg adminSettingsUpdateMsg = (AdminSettingsUpdateMsg) message;
if (key.equals(adminSettingsUpdateMsg.getKey()) if (key.equals(adminSettingsUpdateMsg.getKey())) {
&& isSystem == adminSettingsUpdateMsg.getIsSystem()) {
messages.remove(message); messages.remove(message);
return true; return true;
} }

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

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.io.IOUtils;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -614,6 +616,60 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(status().isForbidden()); .andExpect(status().isForbidden());
} }
@Test
public void testGetLwm2mListObjectsPage() throws Exception {
loginTenantAdmin();
List<TbResource> resources = loadLwm2mResources();
List<LwM2mObject> objects =
doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>(){});
Assert.assertNotNull(objects);
Assert.assertEquals(resources.size(), objects.size());
removeLoadResources(resources);
}
@Test
public void testGetLwm2mListObjects() throws Exception {
loginTenantAdmin();
List<TbResource> resources = loadLwm2mResources();
List<LwM2mObject> objects =
doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>(){});
Assert.assertNotNull(objects);
Assert.assertEquals(3, objects.size());
removeLoadResources(resources);
}
private List<TbResource> loadLwm2mResources() throws Exception {
var models = List.of("1", "2", "3", "5", "6", "9", "19", "3303");
List<TbResource> resources = new ArrayList<>(models.size());
for (String model : models) {
String fileName = model + ".xml";
byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName));
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.LWM2M_MODEL);
resource.setFileName(fileName);
resource.setData(Base64.getEncoder().encodeToString(bytes));
resources.add(save(resource));
}
return resources;
}
private void removeLoadResources(List<TbResource> resources) throws Exception {
for (TbResourceInfo resource : resources) {
doDelete("/api/resource/" + resource.getId().getId().toString())
.andExpect(status().isOk());
}
}
private TbResource save(TbResource tbResource) throws Exception { private TbResource save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() { return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
}); });

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

@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.settings.StarredDashboardInfo; import org.thingsboard.server.common.data.settings.StarredDashboardInfo;
import org.thingsboard.server.common.data.settings.UserDashboardsInfo; import org.thingsboard.server.common.data.settings.UserDashboardsInfo;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.user.UserDao; import org.thingsboard.server.dao.user.UserDao;
@ -81,6 +82,9 @@ public class UserControllerTest extends AbstractControllerTest {
@Autowired @Autowired
private UserDao userDao; private UserDao userDao;
@Autowired
private DeviceService deviceService;
static class Config { static class Config {
@Bean @Bean
@Primary @Primary
@ -740,6 +744,43 @@ public class UserControllerTest extends AbstractControllerTest {
Assert.assertEquals(expectedCustomerUserIds, loadedUserIds); Assert.assertEquals(expectedCustomerUserIds, loadedUserIds);
} }
@Test
public void testGetUsersForDeletedAlarmOriginator() throws Exception {
loginTenantAdmin();
String email = "testEmail1";
for (int i = 0; i < 45; i++) {
User customerUser = createCustomerUser( customerId);
customerUser.setEmail(email + StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)) + "@thingsboard.org");
doPost("/api/user", customerUser, User.class);
}
Device device = new Device();
device.setName("testDevice");
device.setCustomerId(customerId);
Device savedDevice = doPost("/api/device", device, Device.class);
Alarm alarm = createTestAlarm(savedDevice);
deviceService.deleteDevice(tenantId, savedDevice.getId());
List<UserId> loadedUserIds = new ArrayList<>();
PageLink pageLink = new PageLink(33, 0);
PageData<UserEmailInfo> pageData;
do {
pageData = doGetTypedWithPageLink("/api/users/assign/" + alarm.getId().getId().toString() + "?",
new TypeReference<>() {}, pageLink);
loadedUserIds.addAll(pageData.getData().stream().map(UserEmailInfo::getId)
.collect(Collectors.toList()));
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
} while (pageData.hasNext());
Assert.assertEquals(1, loadedUserIds.size());
Assert.assertEquals(tenantAdminUserId, loadedUserIds.get(0));
}
@Test @Test
public void testDeleteUserWithDeleteRelationsOk() throws Exception { public void testDeleteUserWithDeleteRelationsOk() throws Exception {
loginSysAdmin(); loginSysAdmin();

7
application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java

@ -268,6 +268,13 @@ public class WidgetsBundleControllerTest extends AbstractControllerTest {
Collections.sort(loadedWidgetsBundles2, idComparator); Collections.sort(loadedWidgetsBundles2, idComparator);
Assert.assertEquals(tenantWidgetsBundles, loadedWidgetsBundles2); Assert.assertEquals(tenantWidgetsBundles, loadedWidgetsBundles2);
// cleanup
loginSysAdmin();
for (WidgetsBundle sysWidgetsBundle : sysWidgetsBundles) {
doDelete("/api/widgetsBundle/" + sysWidgetsBundle.getId().getId().toString())
.andExpect(status().isOk());
}
} }
@Test @Test

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

@ -17,8 +17,10 @@ package org.thingsboard.server.edge;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite; import com.google.protobuf.MessageLite;
import lombok.extern.slf4j.Slf4j;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -69,6 +71,7 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.edge.imitator.EdgeImitator; import org.thingsboard.server.edge.imitator.EdgeImitator;
@ -96,6 +99,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -104,6 +108,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"queue.rule-engine.stats.enabled=false", "queue.rule-engine.stats.enabled=false",
"edges.storage.sleep_between_batches=1000" "edges.storage.sleep_between_batches=1000"
}) })
@Slf4j
abstract public class AbstractEdgeTest extends AbstractControllerTest { abstract public class AbstractEdgeTest extends AbstractControllerTest {
private static final String THERMOSTAT_DEVICE_PROFILE_NAME = "Thermostat"; private static final String THERMOSTAT_DEVICE_PROFILE_NAME = "Thermostat";
@ -124,6 +129,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@Before @Before
public void setupEdgeTest() throws Exception { public void setupEdgeTest() throws Exception {
loginSysAdmin();
// get jwt settings from yaml config
JwtSettings settings = doGet("/api/admin/jwtSettings", JwtSettings.class);
// save jwt settings into db
doPost("/api/admin/jwtSettings", settings).andExpect(status().isOk());
loginTenantAdmin(); loginTenantAdmin();
installation(); installation();
@ -170,7 +182,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
private void installation() { private void installation() throws Exception {
thermostatDeviceProfile = this.createDeviceProfile(THERMOSTAT_DEVICE_PROFILE_NAME, thermostatDeviceProfile = this.createDeviceProfile(THERMOSTAT_DEVICE_PROFILE_NAME,
createMqttDeviceProfileTransportConfiguration(new JsonTransportPayloadConfiguration(), false)); createMqttDeviceProfileTransportConfiguration(new JsonTransportPayloadConfiguration(), false));
extendDeviceProfileData(thermostatDeviceProfile); extendDeviceProfileData(thermostatDeviceProfile);
@ -186,6 +198,9 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
+ "/device/" + savedDevice.getUuidId(), Device.class); + "/device/" + savedDevice.getUuidId(), Device.class);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/asset/" + savedAsset.getUuidId(), Asset.class); + "/asset/" + savedAsset.getUuidId(), Asset.class);
// wait until assign device and asset events are fully processed by edge notification service
TimeUnit.MILLISECONDS.sleep(500);
} }
protected void extendDeviceProfileData(DeviceProfile deviceProfile) { protected void extendDeviceProfileData(DeviceProfile deviceProfile) {
@ -223,53 +238,75 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateEdgeConfiguration(); validateEdgeConfiguration();
// 1 message from queue fetcher // 1 message from queue fetcher
validateMsgsCnt(QueueUpdateMsg.class, 1);
validateQueues(); validateQueues();
// 1 from rule chain fetcher // 1 from rule chain fetcher
validateMsgsCnt(RuleChainUpdateMsg.class, 1);
UUID ruleChainUUID = validateRuleChains(); UUID ruleChainUUID = validateRuleChains();
// 1 from request message // 1 from request message
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1);
validateRuleChainMetadataUpdates(ruleChainUUID); validateRuleChainMetadataUpdates(ruleChainUUID);
// 4 messages // 4 messages ('general', 'mail', 'connectivity', 'jwt')
// - 2 from fetcher - system level ('mail', 'mailTemplates') validateMsgsCnt(AdminSettingsUpdateMsg.class, 4);
// - 2 from fetcher - admin level ('mail', 'mailTemplates') validateAdminSettings(4);
validateAdminSettings();
// 4 messages // 4 messages
// - 1 from default profile fetcher // - 1 from default profile fetcher
// - 2 from device profile fetcher (default and thermostat) // - 2 from device profile fetcher (default and thermostat)
// - 1 from device fetcher // - 1 from device fetcher
validateDeviceProfiles(); validateMsgsCnt(DeviceProfileUpdateMsg.class, 4);
validateDeviceProfiles(4);
// 3 messages // 3 messages
// - 1 from default profile fetcher // - 1 from default profile fetcher
// - 1 message from asset profile fetcher // - 1 message from asset profile fetcher
// - 1 message from asset fetcher // - 1 message from asset fetcher
validateAssetProfiles(); validateMsgsCnt(AssetProfileUpdateMsg.class, 3);
validateAssetProfiles(3);
// 1 from device fetcher // 1 from device fetcher
validateMsgsCnt(DeviceUpdateMsg.class, 1);
validateDevices(); validateDevices();
// 1 from asset fetcher // 1 from asset fetcher
validateMsgsCnt(AssetUpdateMsg.class, 1);
validateAssets(); validateAssets();
// 1 message from public customer fetcher // 1 message from public customer fetcher
validateMsgsCnt(CustomerUpdateMsg.class, 1);
validatePublicCustomer(); validatePublicCustomer();
// 1 message from user fetcher // 1 message from user fetcher
validateMsgsCnt(UserUpdateMsg.class, 1);
validateUsers(); validateUsers();
// 1 from tenant fetcher // 1 from tenant fetcher
validateMsgsCnt(TenantUpdateMsg.class, 1);
validateTenant(); validateTenant();
// 1 from tenant profile fetcher // 1 from tenant profile fetcher
validateMsgsCnt(TenantProfileUpdateMsg.class, 1);
validateTenantProfile(); validateTenantProfile();
// 1 message sync completed // 1 message sync completed
validateMsgsCnt(SyncCompletedMsg.class, 1);
validateSyncCompleted(); validateSyncCompleted();
} }
private <T extends AbstractMessage> void validateMsgsCnt(Class<T> clazz, int expectedMsgCnt) {
List<T> downlinkMsgsByType = edgeImitator.findAllMessagesByType(clazz);
if (downlinkMsgsByType.size() != expectedMsgCnt) {
List<AbstractMessage> downlinkMsgs = edgeImitator.getDownlinkMsgs();
for (AbstractMessage downlinkMsg : downlinkMsgs) {
log.error("{}\n{}", downlinkMsg.getClass(), downlinkMsg);
}
Assert.fail("Unexpected message count for " + clazz + "! Expected: " + expectedMsgCnt + ", but found: " + downlinkMsgsByType.size());
}
}
private void validateEdgeConfiguration() throws Exception { private void validateEdgeConfiguration() throws Exception {
EdgeConfiguration configuration = edgeImitator.getConfiguration(); EdgeConfiguration configuration = edgeImitator.getConfiguration();
Assert.assertNotNull(configuration); Assert.assertNotNull(configuration);
@ -299,13 +336,13 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
testAutoGeneratedCodeByProtobuf(tenantProfileUpdateMsg); testAutoGeneratedCodeByProtobuf(tenantProfileUpdateMsg);
} }
private void validateDeviceProfiles() throws Exception { private void validateDeviceProfiles(int expectedMsgCnt) throws Exception {
List<DeviceProfileUpdateMsg> deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class); List<DeviceProfileUpdateMsg> deviceProfileUpdateMsgList = edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class);
// default msg default device profile from fetcher // default msg default device profile from fetcher
// default msg device profile from fetcher // default msg device profile from fetcher
// thermostat msg from device profile fetcher // thermostat msg from device profile fetcher
// thermostat msg from device fetcher // thermostat msg from device fetcher
Assert.assertEquals(4, deviceProfileUpdateMsgList.size()); Assert.assertEquals(expectedMsgCnt, deviceProfileUpdateMsgList.size());
Optional<DeviceProfileUpdateMsg> thermostatProfileUpdateMsgOpt = Optional<DeviceProfileUpdateMsg> thermostatProfileUpdateMsgOpt =
deviceProfileUpdateMsgList.stream().filter(dfum -> THERMOSTAT_DEVICE_PROFILE_NAME.equals(dfum.getName())).findAny(); deviceProfileUpdateMsgList.stream().filter(dfum -> THERMOSTAT_DEVICE_PROFILE_NAME.equals(dfum.getName())).findAny();
Assert.assertTrue(thermostatProfileUpdateMsgOpt.isPresent()); Assert.assertTrue(thermostatProfileUpdateMsgOpt.isPresent());
@ -386,20 +423,28 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID); Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID);
} }
private void validateAdminSettings() { private void validateAdminSettings(int expectedMsgCnt) {
List<AdminSettingsUpdateMsg> adminSettingsUpdateMsgs = edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class); List<AdminSettingsUpdateMsg> adminSettingsUpdateMsgs = edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class);
Assert.assertEquals(4, adminSettingsUpdateMsgs.size()); Assert.assertEquals(expectedMsgCnt, adminSettingsUpdateMsgs.size());
for (AdminSettingsUpdateMsg adminSettingsUpdateMsg : adminSettingsUpdateMsgs) { for (AdminSettingsUpdateMsg adminSettingsUpdateMsg : adminSettingsUpdateMsgs) {
if (adminSettingsUpdateMsg.getKey().equals("general")) {
validateGeneralAdminSettings(adminSettingsUpdateMsg);
}
if (adminSettingsUpdateMsg.getKey().equals("mail")) { if (adminSettingsUpdateMsg.getKey().equals("mail")) {
validateMailAdminSettings(adminSettingsUpdateMsg); validateMailAdminSettings(adminSettingsUpdateMsg);
} }
if (adminSettingsUpdateMsg.getKey().equals("mailTemplates")) { if (adminSettingsUpdateMsg.getKey().equals("connectivity")) {
validateMailTemplatesAdminSettings(adminSettingsUpdateMsg); validateConnectivityAdminSettings(adminSettingsUpdateMsg);
} }
} }
} }
private void validateGeneralAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue());
Assert.assertNotNull(jsonNode.get("baseUrl"));
}
private void validateMailAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { private void validateMailAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue());
Assert.assertNotNull(jsonNode.get("mailFrom")); Assert.assertNotNull(jsonNode.get("mailFrom"));
@ -409,19 +454,19 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertNotNull(jsonNode.get("timeout")); Assert.assertNotNull(jsonNode.get("timeout"));
} }
private void validateMailTemplatesAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) { private void validateConnectivityAdminSettings(AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue()); JsonNode jsonNode = JacksonUtil.toJsonNode(adminSettingsUpdateMsg.getJsonValue());
Assert.assertNotNull(jsonNode.get("accountActivated")); Assert.assertNotNull(jsonNode.get("http"));
Assert.assertNotNull(jsonNode.get("accountLockout")); Assert.assertNotNull(jsonNode.get("https"));
Assert.assertNotNull(jsonNode.get("activation")); Assert.assertNotNull(jsonNode.get("mqtt"));
Assert.assertNotNull(jsonNode.get("passwordWasReset")); Assert.assertNotNull(jsonNode.get("mqtts"));
Assert.assertNotNull(jsonNode.get("resetPassword")); Assert.assertNotNull(jsonNode.get("coap"));
Assert.assertNotNull(jsonNode.get("test")); Assert.assertNotNull(jsonNode.get("coaps"));
} }
private void validateAssetProfiles() throws Exception { private void validateAssetProfiles(int expectedMsgCnt) throws Exception {
List<AssetProfileUpdateMsg> assetProfileUpdateMsgs = edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class); List<AssetProfileUpdateMsg> assetProfileUpdateMsgs = edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class);
Assert.assertEquals(3, assetProfileUpdateMsgs.size()); Assert.assertEquals(expectedMsgCnt, assetProfileUpdateMsgs.size());
AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileUpdateMsgs.get(0); AssetProfileUpdateMsg assetProfileUpdateMsg = assetProfileUpdateMsgs.get(0);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType());
UUID assetProfileUUID = new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB()); UUID assetProfileUUID = new UUID(assetProfileUpdateMsg.getIdMSB(), assetProfileUpdateMsg.getIdLSB());

4
application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java

@ -173,7 +173,7 @@ public class AssetEdgeTest extends AbstractEdgeTest {
assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetUpdateMsgBuilder.setName("Asset Edge 2"); assetUpdateMsgBuilder.setName("Asset Edge 2");
assetUpdateMsgBuilder.setType("test"); assetUpdateMsgBuilder.setType("default");
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());
@ -205,7 +205,7 @@ public class AssetEdgeTest extends AbstractEdgeTest {
assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
assetUpdateMsgBuilder.setName(assetOnCloudName); assetUpdateMsgBuilder.setName(assetOnCloudName);
assetUpdateMsgBuilder.setType("test"); assetUpdateMsgBuilder.setType("default");
assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder);
uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build());

4
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -492,7 +492,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
deviceUpdateMsgBuilder.setName(deviceOnCloudName); deviceUpdateMsgBuilder.setName(deviceOnCloudName);
deviceUpdateMsgBuilder.setType("test"); deviceUpdateMsgBuilder.setType("default");
deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder); testAutoGeneratedCodeByProtobuf(deviceUpdateMsgBuilder);
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());
@ -541,7 +541,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
deviceUpdateMsgBuilder.setName("Edge Device 2"); deviceUpdateMsgBuilder.setName("Edge Device 2");
deviceUpdateMsgBuilder.setType("test"); deviceUpdateMsgBuilder.setType("default");
deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());

51
application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -120,4 +121,54 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
Assert.assertNotNull(tbResource); Assert.assertNotNull(tbResource);
Assert.assertEquals("Edge Test Resource", tbResource.getName()); Assert.assertEquals("Edge Test Resource", tbResource.getName());
} }
@Test
public void testResourceToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle("Edge Test Resource");
resource.setFileName(FILE_NAME);
resource.setData(TEST_DATA);
edgeImitator.expectMessageAmount(1);
TbResource savedResource = doPost("/api/resource", resource, TbResource.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
ResourceUpdateMsg.Builder resourceUpdateMsgBuilder = ResourceUpdateMsg.newBuilder();
resourceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
resourceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
resourceUpdateMsgBuilder.setTitle("Edge Test Resource");
resourceUpdateMsgBuilder.setResourceType(ResourceType.JKS.name());
resourceUpdateMsgBuilder.setResourceKey(FILE_NAME);
resourceUpdateMsgBuilder.setFileName(FILE_NAME);
resourceUpdateMsgBuilder.setData(TEST_DATA);
resourceUpdateMsgBuilder.setIsSystem(false);
resourceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
testAutoGeneratedCodeByProtobuf(resourceUpdateMsgBuilder);
uplinkMsgBuilder.addResourceUpdateMsg(resourceUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<ResourceUpdateMsg> resourceUpdateMsgOpt = edgeImitator.findMessageByType(ResourceUpdateMsg.class);
Assert.assertTrue(resourceUpdateMsgOpt.isPresent());
ResourceUpdateMsg latestResourceUpdateMsg = resourceUpdateMsgOpt.get();
Assert.assertNotEquals(FILE_NAME, latestResourceUpdateMsg.getResourceKey());
Assert.assertNotEquals(savedResource.getUuidId(), uuid);
TbResource tbResource = doGet("/api/resource/" + uuid, TbResource.class);
Assert.assertNotNull(tbResource);
Assert.assertNotEquals(FILE_NAME, tbResource.getName());
}
} }

2
application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java

@ -24,6 +24,7 @@ import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
@ -144,6 +145,7 @@ public class NotificationTargetApiTest extends AbstractNotificationApiTest {
notificationTarget.setConfiguration(targetConfig); notificationTarget.setConfiguration(targetConfig);
save(notificationTarget, status().isOk()); save(notificationTarget, status().isOk());
assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isNotEmpty(); assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isNotEmpty();
assertThat(notificationTargetDao.findByTenantIdAndSupportedNotificationTypeAndPageLink(differentTenantId, NotificationType.GENERAL, new PageLink(10)).getData()).isNotEmpty();
deleteDifferentTenant(); deleteDifferentTenant();
assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty(); assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty();

4
application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java

@ -458,7 +458,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
verify(consumer2, never()).unsubscribe(); verify(consumer2, never()).unsubscribe();
int msgCount = totalConsumedMsgs.get(); int msgCount = totalConsumedMsgs.get();
await().atLeast(4, TimeUnit.SECONDS) // based on topicDeletionDelayInSec await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call)
.atMost(7, TimeUnit.SECONDS) .atMost(7, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
partitions.stream() partitions.stream()
@ -498,7 +498,7 @@ public class TbRuleEngineQueueConsumerManagerTest {
verify(consumer, never()).unsubscribe(); verify(consumer, never()).unsubscribe();
int msgCount = totalConsumedMsgs.get(); int msgCount = totalConsumedMsgs.get();
await().atLeast(4, TimeUnit.SECONDS) await().atLeast(2, TimeUnit.SECONDS) // based on topicDeletionDelayInSec(5) = 5 - ( 3 seconds the code may execute starting consumerManager.delete() call)
.atMost(7, TimeUnit.SECONDS) .atMost(7, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
partitions.stream() partitions.stream()

13
common/cache/src/main/java/org/thingsboard/server/cache/device/DeviceCacheKey.java

@ -34,6 +34,10 @@ public class DeviceCacheKey implements Serializable {
private final DeviceId deviceId; private final DeviceId deviceId;
private final String deviceName; private final String deviceName;
public DeviceCacheKey(DeviceId deviceId) {
this(null, deviceId, null);
}
public DeviceCacheKey(TenantId tenantId, DeviceId deviceId) { public DeviceCacheKey(TenantId tenantId, DeviceId deviceId) {
this(tenantId, deviceId, null); this(tenantId, deviceId, null);
} }
@ -44,11 +48,12 @@ public class DeviceCacheKey implements Serializable {
@Override @Override
public String toString() { public String toString() {
if (deviceId != null) { if (deviceId == null) {
return tenantId + "_" + deviceId;
} else {
return tenantId + "_n_" + deviceName; return tenantId + "_n_" + deviceName;
} else if (tenantId == null) {
return deviceId.toString();
} else {
return tenantId + "_" + deviceId;
} }
} }
} }

8
common/cluster-api/src/main/proto/queue.proto

@ -910,8 +910,8 @@ message EdgeNotificationMsgProto {
string body = 10; string body = 10;
PostTelemetryMsg postTelemetryMsg = 11; PostTelemetryMsg postTelemetryMsg = 11;
PostAttributeMsg postAttributesMsg = 12; PostAttributeMsg postAttributesMsg = 12;
int64 sourceEdgeIdMSB = 13; int64 originatorEdgeIdMSB = 13;
int64 sourceEdgeIdLSB = 14; int64 originatorEdgeIdLSB = 14;
} }
message EdgeEventUpdateMsgProto { message EdgeEventUpdateMsgProto {
@ -945,8 +945,8 @@ message DeviceEdgeUpdateMsgProto {
int64 tenantIdLSB = 2; int64 tenantIdLSB = 2;
int64 deviceIdMSB = 3; int64 deviceIdMSB = 3;
int64 deviceIdLSB = 4; int64 deviceIdLSB = 4;
int64 edgeIdMSB = 5; optional int64 edgeIdMSB = 5;
int64 edgeIdLSB = 6; optional int64 edgeIdLSB = 6;
} }
message DeviceNameOrTypeUpdateMsgProto { message DeviceNameOrTypeUpdateMsgProto {

2
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -111,7 +111,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey) .setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret) .setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_3_6_0) .setEdgeVersion(EdgeVersion.V_3_6_1)
.setMaxInboundMessageSize(maxInboundMessageSize) .setMaxInboundMessageSize(maxInboundMessageSize)
.build()) .build())
.build()); .build());

5
common/edge-api/src/main/proto/edge.proto

@ -35,6 +35,7 @@ enum EdgeVersion {
V_3_3_3 = 1; V_3_3_3 = 1;
V_3_4_0 = 2; V_3_4_0 = 2;
V_3_6_0 = 3; V_3_6_0 = 3;
V_3_6_1 = 4;
} }
/** /**
@ -361,8 +362,8 @@ message WidgetsBundleUpdateMsg {
optional bytes image = 6; optional bytes image = 6;
bool isSystem = 7; bool isSystem = 7;
optional string description = 8; optional string description = 8;
optional int32 order = 9; optional string widgets = 9;
optional string widgets = 10; optional int32 order = 10;
} }
message WidgetTypeUpdateMsg { message WidgetTypeUpdateMsg {

3
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -218,10 +218,9 @@ public class TbDate implements Serializable, Cloneable {
return opt; return opt;
} }
public long now() { public static long now() {
return Instant.now().toEpochMilli(); return Instant.now().toEpochMilli();
} }
public long parseSecond() { public long parseSecond() {
return instant.getEpochSecond(); return instant.getEpochSecond();
} }

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

@ -61,9 +61,9 @@ public class TbUtils {
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson", parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, Object.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes", parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class, String.class))); ExecutionContext.class, Object.class, String.class)));
parserConfig.registerNonConvertableMethods(TbUtils.class, Collections.singleton("stringToBytes")); parserConfig.registerNonConvertableMethods(TbUtils.class, Collections.singleton("stringToBytes"));
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt", parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",
String.class))); String.class)));
@ -192,14 +192,22 @@ public class TbUtils {
return new String(bytes, charsetName); return new String(bytes, charsetName);
} }
public static List<Byte> stringToBytes(ExecutionContext ctx, String str) { public static List<Byte> stringToBytes(ExecutionContext ctx, Object str) throws IllegalAccessException {
byte[] bytes = str.getBytes(); if (str instanceof String) {
return bytesToList(ctx, bytes); byte[] bytes = str.toString().getBytes();
return bytesToList(ctx, bytes);
} else {
throw new IllegalAccessException("Invalid type parameter [" + str.getClass().getSimpleName() + "]. Expected 'String'");
}
} }
public static List<Byte> stringToBytes(ExecutionContext ctx, String str, String charsetName) throws UnsupportedEncodingException { public static List<Byte> stringToBytes(ExecutionContext ctx, Object str, String charsetName) throws UnsupportedEncodingException, IllegalAccessException {
byte[] bytes = str.getBytes(charsetName); if (str instanceof String) {
return bytesToList(ctx, bytes); byte[] bytes = str.toString().getBytes(charsetName);
return bytesToList(ctx, bytes);
} else {
throw new IllegalAccessException("Invalid type parameter [" + str.getClass().getSimpleName() + "]. Expected 'String'");
}
} }
private static byte[] bytesFromList(List<Byte> bytesList) { private static byte[] bytesFromList(List<Byte> bytesList) {

9
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java

@ -21,6 +21,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.assertj.core.data.Offset;
import org.junit.Assert; import org.junit.Assert;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -28,8 +30,8 @@ import org.mvel2.ConversionException;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -764,6 +766,11 @@ class TbDateTest {
Assert.assertEquals("22:15:30 Eastern Standard Time", d1.toTimeString("UTC", "America/New_York")); Assert.assertEquals("22:15:30 Eastern Standard Time", d1.toTimeString("UTC", "America/New_York"));
} }
@Test
public void testNow() {
Assertions.assertThat(TbDate.now()).isCloseTo(Instant.now().toEpochMilli(), Offset.offset(1000L));
}
private void testResultChangeDateTime(TbDate d) { private void testResultChangeDateTime(TbDate d) {
int localOffset = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds(); int localOffset = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds();
TbDateTestEntity tbDateTestEntity = new TbDateTestEntity(d.getFullYear(), d.getMonth(), d.getDate(), (d.getHours() - (localOffset/60/60))); TbDateTestEntity tbDateTestEntity = new TbDateTestEntity(d.getFullYear(), d.getMonth(), d.getDate(), (d.getHours() - (localOffset/60/60)));

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

@ -350,7 +350,7 @@ public class TbUtilsTest {
} }
@Test @Test
public void parseBytesDecodeToJson() throws IOException { public void parseBytesDecodeToJson() throws IOException, IllegalAccessException {
String expectedStr = "{\"hello\": \"world\"}"; String expectedStr = "{\"hello\": \"world\"}";
ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx); ExecutionHashMap<String, Object> expectedJson = new ExecutionHashMap<>(1, ctx);
expectedJson.put("hello", "world"); expectedJson.put("hello", "world");
@ -367,6 +367,24 @@ public class TbUtilsTest {
Assert.assertEquals(expectedJson,actualJson); Assert.assertEquals(expectedJson,actualJson);
} }
@Test
public void stringToBytesInputObjectTest() throws IOException, IllegalAccessException {
String expectedStr = "{\"hello\": \"world\"}";
Object inputJson = TbUtils.decodeToJson(ctx, expectedStr);
byte[] arrayBytes = {119, 111, 114, 108, 100};
List<Byte> expectedBytes = Bytes.asList(arrayBytes);
List<Byte> actualBytes = TbUtils.stringToBytes(ctx, ((ExecutionHashMap) inputJson).get("hello"));
Assert.assertEquals(expectedBytes, actualBytes);
actualBytes = TbUtils.stringToBytes(ctx, ((ExecutionHashMap) inputJson).get("hello"), "UTF-8");
Assert.assertEquals(expectedBytes, actualBytes);
expectedStr = "{\"hello\": 1234}";
inputJson = TbUtils.decodeToJson(ctx, expectedStr);
Object finalInputJson = inputJson;
Assert.assertThrows(IllegalAccessException.class, () -> TbUtils.stringToBytes(ctx, ((ExecutionHashMap) finalInputJson).get("hello")));
Assert.assertThrows(IllegalAccessException.class, () -> TbUtils.stringToBytes(ctx, ((ExecutionHashMap) finalInputJson).get("hello"), "UTF-8"));
}
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length); List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) { for (Byte b : data) {

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

@ -188,6 +188,7 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
if (alarm == null) { if (alarm == null) {
return AlarmApiCallResult.builder().successful(false).build(); return AlarmApiCallResult.builder().successful(false).build();
} else { } else {
var propagationIds = getPropagationEntityIdsList(alarm);
deleteEntityRelations(tenantId, alarm.getId()); deleteEntityRelations(tenantId, alarm.getId());
alarmDao.removeById(tenantId, alarm.getUuidId()); alarmDao.removeById(tenantId, alarm.getUuidId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId) eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId)
@ -195,7 +196,7 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
if (checkAndDeleteAlarmType) { if (checkAndDeleteAlarmType) {
delAlarmTypes(tenantId, Collections.singleton(alarm.getType())); delAlarmTypes(tenantId, Collections.singleton(alarm.getType()));
} }
return AlarmApiCallResult.builder().alarm(alarm).deleted(true).successful(true).build(); return AlarmApiCallResult.builder().alarm(alarm).deleted(true).successful(true).propagatedEntitiesList(propagationIds).build();
} }
} }

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

@ -130,15 +130,13 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
public Device findDeviceById(TenantId tenantId, DeviceId deviceId) { public Device findDeviceById(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId); log.trace("Executing findDeviceById [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, deviceId), if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
() -> { return cache.getAndPutInTransaction(new DeviceCacheKey(deviceId),
//TODO: possible bug source since sometimes we need to clear cache by tenant id and sometimes by sys tenant id? () -> deviceDao.findById(tenantId, deviceId.getId()), true);
if (TenantId.SYS_TENANT_ID.equals(tenantId)) { } else {
return deviceDao.findById(tenantId, deviceId.getId()); return cache.getAndPutInTransaction(new DeviceCacheKey(tenantId, deviceId),
} else { () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()), true);
return deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId()); }
}
}, true);
} }
@Override @Override
@ -258,6 +256,7 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
List<DeviceCacheKey> keys = new ArrayList<>(3); List<DeviceCacheKey> keys = new ArrayList<>(3);
keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewName())); keys.add(new DeviceCacheKey(event.getTenantId(), event.getNewName()));
if (event.getDeviceId() != null) { if (event.getDeviceId() != null) {
keys.add(new DeviceCacheKey(event.getDeviceId()));
keys.add(new DeviceCacheKey(event.getTenantId(), event.getDeviceId())); keys.add(new DeviceCacheKey(event.getTenantId(), event.getDeviceId()));
} }
if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) {

2
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationTargetRepository.java

@ -39,7 +39,7 @@ public interface NotificationTargetRepository extends JpaRepository<Notification
Pageable pageable); Pageable pageable);
@Query(value = "SELECT * FROM notification_target t WHERE t.tenant_id = :tenantId " + @Query(value = "SELECT * FROM notification_target t WHERE t.tenant_id = :tenantId " +
"AND (:searchText IS NULL OR ilike(t.name, concat('%', :searchText, '%')) = true) " + "AND (:searchText IS NULL OR t.name ILIKE concat('%', :searchText, '%')) " +
"AND (cast(t.configuration as json) ->> 'type' <> 'PLATFORM_USERS' OR " + "AND (cast(t.configuration as json) ->> 'type' <> 'PLATFORM_USERS' OR " +
"cast(t.configuration as json) -> 'usersFilter' ->> 'type' IN :usersFilterTypes)", nativeQuery = true) "cast(t.configuration as json) -> 'usersFilter' ->> 'type' IN :usersFilterTypes)", nativeQuery = true)
Page<NotificationTargetEntity> findByTenantIdAndSearchTextAndUsersFilterTypeIfPresent(@Param("tenantId") UUID tenantId, Page<NotificationTargetEntity> findByTenantIdAndSearchTextAndUsersFilterTypeIfPresent(@Param("tenantId") UUID tenantId,

4
dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java

@ -45,7 +45,7 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
@Param("tenantId") UUID tenantId, @Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysAdminId, @Param("systemAdminId") UUID sysAdminId,
@Param("resourceType") String resourceType, @Param("resourceType") String resourceType,
@Param("searchText") String search, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query("SELECT tr FROM TbResourceEntity tr " + @Query("SELECT tr FROM TbResourceEntity tr " +
@ -61,7 +61,7 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
List<TbResourceEntity> findResources(@Param("tenantId") UUID tenantId, List<TbResourceEntity> findResources(@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysAdminId, @Param("systemAdminId") UUID sysAdminId,
@Param("resourceType") String resourceType, @Param("resourceType") String resourceType,
@Param("searchText") String search); @Param("searchText") String searchText);
@Query("SELECT tr FROM TbResourceEntity tr " + @Query("SELECT tr FROM TbResourceEntity tr " +
"WHERE tr.resourceType = :resourceType " + "WHERE tr.resourceType = :resourceType " +

18
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -20,6 +20,9 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.Arrays;
import java.util.List;
public class DeviceConnectivityUtil { public class DeviceConnectivityUtil {
public static final String HTTP = "http"; public static final String HTTP = "http";
@ -38,6 +41,7 @@ public class DeviceConnectivityUtil {
public static final String GATEWAY_DOCKER_RUN = "docker run -it "; public static final String GATEWAY_DOCKER_RUN = "docker run -it ";
public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients "; public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients ";
public static final String COAP_IMAGE = "thingsboard/coap-clients "; public static final String COAP_IMAGE = "thingsboard/coap-clients ";
public static final List<String> LOCAL_HOSTS = Arrays.asList("localhost", "127.0.0.1");
public static String getHttpPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) { public static String getHttpPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
return String.format("curl -v -X POST %s://%s%s/api/v1/%s/telemetry --header Content-Type:application/json --data " + JSON_EXAMPLE_PAYLOAD, return String.format("curl -v -X POST %s://%s%s/api/v1/%s/telemetry --header Content-Type:application/json --data " + JSON_EXAMPLE_PAYLOAD,
@ -54,20 +58,20 @@ public class DeviceConnectivityUtil {
switch (deviceCredentials.getCredentialsType()) { switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN: case ACCESS_TOKEN:
command.append(" -u ").append(deviceCredentials.getCredentialsId()); command.append(" -u \"").append(deviceCredentials.getCredentialsId()).append("\"");
break; break;
case MQTT_BASIC: case MQTT_BASIC:
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class); BasicMqttCredentials.class);
if (credentials != null) { if (credentials != null) {
if (credentials.getClientId() != null) { if (credentials.getClientId() != null) {
command.append(" -i ").append(credentials.getClientId()); command.append(" -i \"").append(credentials.getClientId()).append("\"");
} }
if (credentials.getUserName() != null) { if (credentials.getUserName() != null) {
command.append(" -u ").append(credentials.getUserName()); command.append(" -u \"").append(credentials.getUserName()).append("\"");
} }
if (credentials.getPassword() != null) { if (credentials.getPassword() != null) {
command.append(" -P ").append(credentials.getPassword()); command.append(" -P \"").append(credentials.getPassword()).append("\"");;
} }
} else { } else {
return null; return null;
@ -135,7 +139,7 @@ public class DeviceConnectivityUtil {
} }
StringBuilder mqttDockerCommand = new StringBuilder(); StringBuilder mqttDockerCommand = new StringBuilder();
mqttDockerCommand.append(DOCKER_RUN).append(MQTT_IMAGE); mqttDockerCommand.append(DOCKER_RUN).append(LOCAL_HOSTS.contains(host) ? "--network=host ":"").append(MQTT_IMAGE);
if (MQTTS.equals(protocol)) { if (MQTTS.equals(protocol)) {
mqttDockerCommand.append("/bin/sh -c \"") mqttDockerCommand.append("/bin/sh -c \"")
@ -158,7 +162,7 @@ public class DeviceConnectivityUtil {
switch (deviceCredentials.getCredentialsType()) { switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN: case ACCESS_TOKEN:
String client = COAPS.equals(protocol) ? "coap-client-openssl" : "coap-client"; String client = COAPS.equals(protocol) ? "coap-client-openssl" : "coap-client";
return String.format("%s -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s", return String.format("%s -v 6 -m POST %s://%s%s/api/v1/%s/telemetry -t json -e %s",
client, protocol, host, port, deviceCredentials.getCredentialsId(), JSON_EXAMPLE_PAYLOAD); client, protocol, host, port, deviceCredentials.getCredentialsId(), JSON_EXAMPLE_PAYLOAD);
default: default:
return null; return null;
@ -167,6 +171,6 @@ public class DeviceConnectivityUtil {
public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) { public static String getDockerCoapPublishCommand(String protocol, String host, String port, DeviceCredentials deviceCredentials) {
String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials); String coapCommand = getCoapPublishCommand(protocol, host, port, deviceCredentials);
return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN, COAP_IMAGE, coapCommand) : null; return coapCommand != null ? String.format("%s%s%s", DOCKER_RUN + (LOCAL_HOSTS.contains(host) ? "--network=host ":""), COAP_IMAGE, coapCommand) : null;
} }
} }

33
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java

@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -74,8 +75,8 @@ public class ThingsBoardDbInstaller {
new File("./../../docker/docker-compose.yml"), new File("./../../docker/docker-compose.yml"),
new File("./../../docker/docker-compose.volumes.yml"), new File("./../../docker/docker-compose.volumes.yml"),
IS_HYBRID_MODE IS_HYBRID_MODE
? new File("./../../docker/docker-compose.hybrid.yml") ? new File("./../../docker/docker-compose.hybrid.yml")
: new File("./../../docker/docker-compose.postgres.yml"), : new File("./../../docker/docker-compose.postgres.yml"),
new File("./../../docker/docker-compose.postgres.volumes.yml"), new File("./../../docker/docker-compose.postgres.volumes.yml"),
resolveRedisComposeFile(), resolveRedisComposeFile(),
resolveRedisComposeVolumesFile() resolveRedisComposeVolumesFile()
@ -156,7 +157,7 @@ public class ThingsBoardDbInstaller {
return env; return env;
} }
public void createVolumes() { public void createVolumes() {
try { try {
dockerCompose.withCommand("volume create " + postgresDataVolume); dockerCompose.withCommand("volume create " + postgresDataVolume);
@ -200,7 +201,7 @@ public class ThingsBoardDbInstaller {
} }
} else if (IS_REDIS_SENTINEL) { } else if (IS_REDIS_SENTINEL) {
additionalServices.append(" redis-master"); additionalServices.append(" redis-master");
dockerCompose.withCommand("volume create " + redisSentinelDataVolume +"-" + "master"); dockerCompose.withCommand("volume create " + redisSentinelDataVolume + "-" + "master");
dockerCompose.invokeDocker(); dockerCompose.invokeDocker();
additionalServices.append(" redis-slave"); additionalServices.append(" redis-slave");
@ -226,7 +227,8 @@ public class ThingsBoardDbInstaller {
try { try {
dockerCompose.withCommand("down -v"); dockerCompose.withCommand("down -v");
dockerCompose.invokeCompose(); dockerCompose.invokeCompose();
} catch (Exception ignored) {} } catch (Exception ignored) {
}
} }
} }
@ -239,15 +241,24 @@ public class ThingsBoardDbInstaller {
copyLogs(tbSnmpTransportLogVolume, "./target/tb-snmp-transport-logs/"); copyLogs(tbSnmpTransportLogVolume, "./target/tb-snmp-transport-logs/");
copyLogs(tbVcExecutorLogVolume, "./target/tb-vc-executor-logs/"); copyLogs(tbVcExecutorLogVolume, "./target/tb-vc-executor-logs/");
dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume + StringJoiner rmVolumesCommand = new StringJoiner(" ")
" " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume + .add("volume rm -f")
" " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume + resolveRedisComposeVolumeLog()); .add(postgresDataVolume)
dockerCompose.invokeDocker(); .add(tbLogVolume)
.add(tbCoapTransportLogVolume)
.add(tbLwm2mTransportLogVolume)
.add(tbHttpTransportLogVolume)
.add(tbMqttTransportLogVolume)
.add(tbSnmpTransportLogVolume)
.add(tbVcExecutorLogVolume)
.add(resolveRedisComposeVolumeLog());
dockerCompose.withCommand(rmVolumesCommand.toString());
} }
private String resolveRedisComposeVolumeLog() { private String resolveRedisComposeVolumeLog() {
if (IS_REDIS_CLUSTER) { if (IS_REDIS_CLUSTER) {
return IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + "-" + i).collect(Collectors.joining()); return IntStream.range(0, 6).mapToObj(i -> redisClusterDataVolume + "-" + i).collect(Collectors.joining());
} }
if (IS_REDIS_SENTINEL) { if (IS_REDIS_SENTINEL) {
return redisSentinelDataVolume + "-" + "master " + " " + return redisSentinelDataVolume + "-" + "master " + " " +
@ -266,7 +277,7 @@ public class ThingsBoardDbInstaller {
dockerCompose.withCommand("run -d --rm --name " + logsContainerName + " -v " + volumeName + ":/root alpine tail -f /dev/null"); dockerCompose.withCommand("run -d --rm --name " + logsContainerName + " -v " + volumeName + ":/root alpine tail -f /dev/null");
dockerCompose.invokeDocker(); dockerCompose.invokeDocker();
dockerCompose.withCommand("cp " + logsContainerName + ":/root/. "+tbLogsDir.getAbsolutePath()); dockerCompose.withCommand("cp " + logsContainerName + ":/root/. " + tbLogsDir.getAbsolutePath());
dockerCompose.invokeDocker(); dockerCompose.invokeDocker();
dockerCompose.withCommand("rm -f " + logsContainerName); dockerCompose.withCommand("rm -f " + logsContainerName);

14
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/WsClient.java

@ -33,8 +33,8 @@ public class WsClient extends WebSocketClient {
private WsTelemetryResponse message; private WsTelemetryResponse message;
private volatile boolean firstReplyReceived; private volatile boolean firstReplyReceived;
private CountDownLatch firstReply = new CountDownLatch(1); private final CountDownLatch firstReply = new CountDownLatch(1);
private CountDownLatch latch = new CountDownLatch(1); private final CountDownLatch latch = new CountDownLatch(1);
private final long timeoutMultiplier; private final long timeoutMultiplier;
@ -48,7 +48,8 @@ public class WsClient extends WebSocketClient {
} }
@Override @Override
public void onMessage(String message) { public synchronized void onMessage(String message) {
log.error("WS onMessage: {}", message);
if (!firstReplyReceived) { if (!firstReplyReceived) {
firstReplyReceived = true; firstReplyReceived = true;
firstReply.countDown(); firstReply.countDown();
@ -66,12 +67,13 @@ public class WsClient extends WebSocketClient {
} }
@Override @Override
public void onClose(int code, String reason, boolean remote) { public synchronized void onClose(int code, String reason, boolean remote) {
log.info("ws is closed, due to [{}]", reason); log.error("WS onClose: [{}]", reason);
} }
@Override @Override
public void onError(Exception ex) { public synchronized void onError(Exception ex) {
log.error("WS onError: ", ex);
ex.printStackTrace(); ex.printStackTrace();
} }

1
msa/black-box-tests/src/test/resources/logback.xml

@ -25,6 +25,7 @@
</encoder> </encoder>
</appender> </appender>
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>

58
msa/black-box-tests/src/test/resources/tb-node/conf/logback.xml

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration scan="true" scanPeriod="10 seconds">
<appender name="fileLogAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.thingsboard.server" level="INFO" />
<logger name="com.google.common.util.concurrent.AggregateFuture" level="OFF" />
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/>
<logger name="org.apache.kafka.clients" level="WARN"/>
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
<logger name="org.thingsboard.server.service" level="DEBUG"/>
<logger name="org.thingsboard.server.service.subscription" level="TRACE"/>
<root level="INFO">
<appender-ref ref="fileLogAppender"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>

57
msa/black-box-tests/src/test/resources/tb-transports/coap/conf/logback.xml

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration scan="true" scanPeriod="10 seconds">
<appender name="fileLogAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.thingsboard.server" level="INFO" />
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/>
<logger name="org.apache.kafka.clients" level="WARN"/>
<logger name="org.thingsboard.server.transport.coap" level="TRACE"/>
<root level="INFO">
<appender-ref ref="fileLogAppender"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>

57
msa/black-box-tests/src/test/resources/tb-transports/http/conf/logback.xml

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration scan="true" scanPeriod="10 seconds">
<appender name="fileLogAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.thingsboard.server" level="INFO" />
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/>
<logger name="org.apache.kafka.clients" level="WARN"/>
<logger name="org.thingsboard.server.transport.http" level="TRACE"/>
<root level="INFO">
<appender-ref ref="fileLogAppender"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>

55
msa/black-box-tests/src/test/resources/tb-transports/mqtt/conf/logback.xml

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE configuration>
<configuration scan="true" scanPeriod="10 seconds">
<appender name="fileLogAppender"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.thingsboard.server" level="INFO" />
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
<logger name="org.apache.kafka.common.utils.AppInfoParser" level="WARN"/>
<logger name="org.apache.kafka.clients" level="WARN"/>
<logger name="org.thingsboard.server.transport.mqtt" level="TRACE"/>
<root level="INFO">
<appender-ref ref="fileLogAppender"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>

2
msa/tb/pom.xml

@ -38,7 +38,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name> <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name> <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.6.0</pkg.upgradeVersion> <pkg.upgradeVersion>3.6.1</pkg.upgradeVersion>
</properties> </properties>
<dependencies> <dependencies>

3
packaging/java/scripts/control/deb/preinst

@ -10,7 +10,6 @@ if ! getent passwd ${pkg.user} >/dev/null; then
adduser --quiet \ adduser --quiet \
--system \ --system \
--ingroup ${pkg.user} \ --ingroup ${pkg.user} \
--quiet \
--disabled-login \ --disabled-login \
--disabled-password \ --disabled-password \
--home ${pkg.installFolder} \ --home ${pkg.installFolder} \
@ -19,4 +18,4 @@ if ! getent passwd ${pkg.user} >/dev/null; then
${pkg.user} ${pkg.user}
fi fi
exit 0 exit 0

1
packaging/js/scripts/control/deb/preinst

@ -8,7 +8,6 @@ if ! getent passwd ${pkg.user} >/dev/null; then
adduser --quiet \ adduser --quiet \
--system \ --system \
--ingroup ${pkg.user} \ --ingroup ${pkg.user} \
--quiet \
--disabled-login \ --disabled-login \
--disabled-password \ --disabled-password \
--home ${pkg.installFolder} \ --home ${pkg.installFolder} \

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

@ -194,7 +194,7 @@ public class TbHttpClient {
config.isIgnoreRequestBody()) { config.isIgnoreRequestBody()) {
entity = new HttpEntity<>(headers); entity = new HttpEntity<>(headers);
} else { } else {
entity = new HttpEntity<>(getData(msg), headers); entity = new HttpEntity<>(getData(msg, config.isIgnoreRequestBody(), config.isParseToPlainText()), headers);
} }
URI uri = buildEncodedUri(endpointUrl); URI uri = buildEncodedUri(endpointUrl);
@ -242,12 +242,19 @@ public class TbHttpClient {
return uri; return uri;
} }
private String getData(TbMsg msg) { private String getData(TbMsg tbMsg, boolean ignoreBody, boolean parseToPlainText) {
String data = msg.getData(); if (!ignoreBody && parseToPlainText) {
return parseJsonStringToPlainText(tbMsg.getData());
}
return tbMsg.getData();
}
if (config.isTrimDoubleQuotes()) { protected String parseJsonStringToPlainText(String data) {
if (data.startsWith("\"") && data.endsWith("\"") && data.length() >= 2) {
final String dataBefore = data; final String dataBefore = data;
data = data.replaceAll("^\"|\"$", ""); try {
data = JacksonUtil.fromString(data, String.class);
} catch (Exception ignored) {}
log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data); log.trace("Trimming double quotes. Before trim: [{}], after trim: [{}]", dataBefore, data);
} }

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

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.rule.engine.rest; package org.thingsboard.rule.engine.rest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
@ -23,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@Slf4j @Slf4j
@ -30,6 +33,7 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "rest api call", name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class, configClazz = TbRestApiCallNodeConfiguration.class,
version = 1,
nodeDescription = "Invoke REST API calls to external REST server", nodeDescription = "Invoke REST API calls to external REST server",
nodeDetails = "Will invoke REST API call <code>GET | POST | PUT | DELETE</code> to external REST server. " + nodeDetails = "Will invoke REST API call <code>GET | POST | PUT | DELETE</code> to external REST server. " +
"Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." + "Message payload added into Request body. Configured attributes can be added into Headers from Message Metadata." +
@ -45,6 +49,8 @@ import org.thingsboard.server.common.msg.TbMsg;
) )
public class TbRestApiCallNode extends TbAbstractExternalNode { public class TbRestApiCallNode extends TbAbstractExternalNode {
static final String PARSE_TO_PLAIN_TEXT = "parseToPlainText";
static final String TRIM_DOUBLE_QUOTES = "trimDoubleQuotes";
protected TbHttpClient httpClient; protected TbHttpClient httpClient;
@Override @Override
@ -72,4 +78,21 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
} }
} }
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
switch (fromVersion) {
case 0:
if (!oldConfiguration.has(PARSE_TO_PLAIN_TEXT) && oldConfiguration.has(TRIM_DOUBLE_QUOTES)) {
hasChanges = true;
((ObjectNode) oldConfiguration).put(PARSE_TO_PLAIN_TEXT, oldConfiguration.get(TRIM_DOUBLE_QUOTES).booleanValue());
((ObjectNode) oldConfiguration).remove(TRIM_DOUBLE_QUOTES);
}
break;
default:
break;
}
return new TbPair<>(hasChanges, oldConfiguration);
}
} }

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

@ -37,7 +37,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
private int readTimeoutMs; private int readTimeoutMs;
private int maxParallelRequestsCount; private int maxParallelRequestsCount;
private boolean useRedisQueueForMsgPersistence; private boolean useRedisQueueForMsgPersistence;
private boolean trimDoubleQuotes; private boolean parseToPlainText;
private boolean enableProxy; private boolean enableProxy;
private boolean useSystemProxyProperties; private boolean useSystemProxyProperties;
private String proxyHost; private String proxyHost;
@ -58,7 +58,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setReadTimeoutMs(0); configuration.setReadTimeoutMs(0);
configuration.setMaxParallelRequestsCount(0); configuration.setMaxParallelRequestsCount(0);
configuration.setUseRedisQueueForMsgPersistence(false); configuration.setUseRedisQueueForMsgPersistence(false);
configuration.setTrimDoubleQuotes(false); configuration.setParseToPlainText(false);
configuration.setEnableProxy(false); configuration.setEnableProxy(false);
configuration.setCredentials(new AnonymousCredentials()); configuration.setCredentials(new AnonymousCredentials());
configuration.setIgnoreRequestBody(false); configuration.setIgnoreRequestBody(false);

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

File diff suppressed because one or more lines are too long

45
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java

@ -18,17 +18,19 @@ package org.thingsboard.rule.engine.rest;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.After; import org.junit.jupiter.api.AfterEach;
import org.junit.Assert; import org.junit.jupiter.api.Assertions;
import org.junit.Before; import org.junit.jupiter.api.BeforeEach;
import org.junit.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockserver.integration.ClientAndServer; import org.mockserver.integration.ClientAndServer;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.AsyncRestTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -46,6 +48,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -61,13 +64,13 @@ public class TbHttpClientTest {
EventLoopGroup eventLoop; EventLoopGroup eventLoop;
TbHttpClient client; TbHttpClient client;
@Before @BeforeEach
public void setUp() throws Exception { public void setUp() throws Exception {
client = mock(TbHttpClient.class); client = mock(TbHttpClient.class);
willCallRealMethod().given(client).getSharedOrCreateEventLoopGroup(any()); when(client.getSharedOrCreateEventLoopGroup(any())).thenCallRealMethod();
} }
@After @AfterEach
public void tearDown() throws Exception { public void tearDown() throws Exception {
if (eventLoop != null) { if (eventLoop != null) {
eventLoop.shutdownGracefully(); eventLoop.shutdownGracefully();
@ -91,7 +94,7 @@ public class TbHttpClientTest {
Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod(); Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
String url = "http://localhost:8080/"; String url = "http://localhost:8080/";
URI uri = client.buildEncodedUri(url); URI uri = client.buildEncodedUri(url);
Assert.assertEquals(url, uri.toString()); Assertions.assertEquals(url, uri.toString());
} }
@Test @Test
@ -114,7 +117,7 @@ public class TbHttpClientTest {
String url = "http://192.168.1.1/data?d={\"a\": 12}"; String url = "http://192.168.1.1/data?d={\"a\": 12}";
String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D"; String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D";
URI uri = client.buildEncodedUri(url); URI uri = client.buildEncodedUri(url);
Assert.assertEquals(expected, uri.toString()); Assertions.assertEquals(expected, uri.toString());
} }
@Test @Test
@ -183,7 +186,7 @@ public class TbHttpClientTest {
verify(ctx, times(1)).tellSuccess(any()); verify(ctx, times(1)).tellSuccess(any());
verify(ctx, times(0)).tellFailure(any(), any()); verify(ctx, times(0)).tellFailure(any(), any());
Assert.assertEquals(successResponseBody, capturedData.getValue()); Assertions.assertEquals(successResponseBody, capturedData.getValue());
} }
private ClientAndServer setUpDummyServer(String host, String path, String paramKey, String paramVal, String successResponseBody) { private ClientAndServer setUpDummyServer(String host, String path, String paramKey, String paramVal, String successResponseBody) {
@ -219,9 +222,21 @@ public class TbHttpClientTest {
Map<String, String> data = metaData.getData(); Map<String, String> data = metaData.getData();
Assertions.assertThat(data).hasSize(2); Assertions.assertEquals(2, data.size());
Assertions.assertThat(data.get("Content-Type")).isEqualTo("binary"); Assertions.assertEquals(data.get("Content-Type"), "binary");
Assertions.assertThat(data.get("Set-Cookie")).isEqualTo("[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]"); Assertions.assertEquals(data.get("Set-Cookie"), "[\"sap-context=sap-client=075; path=/\",\"sap-token=sap-client=075; path=/\"]");
} }
} @ParameterizedTest
@ValueSource(strings = { "false", "\"", "\"\"", "\"This is a string with double quotes\"", "Path: /home/developer/test.txt",
"First line\nSecond line\n\nFourth line", "Before\rAfter", "Tab\tSeparated\tValues", "Test\bbackspace", "[]",
"[1, 2, 3]", "{\"key\": \"value\"}", "{\n\"temperature\": 25.5,\n\"humidity\": 50.2\n\"}", "Expression: (a + b) * c",
"世界", "Україна", "\u1F1FA\u1F1E6", "🇺🇦"})
public void testParseJsonStringToPlainText(String original) {
Mockito.when(client.parseJsonStringToPlainText(anyString())).thenCallRealMethod();
String serialized = JacksonUtil.toString(original);
Assertions.assertNotNull(serialized);
Assertions.assertEquals(original, client.parseJsonStringToPlainText(serialized));
}
}

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

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.rest; package org.thingsboard.rule.engine.rest;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.http.HttpException; import org.apache.http.HttpException;
import org.apache.http.HttpRequest; import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
@ -26,6 +27,7 @@ import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler; import org.apache.http.protocol.HttpRequestHandler;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
@ -39,6 +41,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -91,7 +94,9 @@ public class TbRestApiCallNodeTest {
@After @After
public void teardown() { public void teardown() {
server.stop(); if (server != null) {
server.stop();
}
} }
@Test @Test
@ -211,4 +216,20 @@ public class TbRestApiCallNodeTest {
assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue());
} }
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration();
var node = new TbRestApiCallNode();
String oldConfig = "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," +
"\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," +
"\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," +
"\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," +
"\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," +
"\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":true}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertTrue(JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()).isParseToPlainText());
}
} }

3
ui-ngx/package.json

@ -5,7 +5,7 @@
"ng": "ng", "ng": "ng",
"start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",
"build": "ng build", "build": "ng build",
"build:prod": "node --max_old_space_size=3072 ./node_modules/@angular/cli/bin/ng build --configuration production --vendor-chunk", "build:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --configuration production --vendor-chunk",
"build:types": "node generate-types.js", "build:types": "node generate-types.js",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
@ -54,6 +54,7 @@
"core-js": "^3.29.1", "core-js": "^3.29.1",
"date-fns": "2.0.0-alpha.27", "date-fns": "2.0.0-alpha.27",
"dayjs": "1.11.4", "dayjs": "1.11.4",
"echarts": "^5.4.3",
"flot": "https://github.com/thingsboard/flot.git#0.9-work", "flot": "https://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master", "flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",

4
ui-ngx/src/app/core/api/widget-subscription.ts

@ -1537,7 +1537,7 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
if (this.type === widgetType.latest) { if (this.type === widgetType.latest) {
const prevData = currentData.data; const prevData = currentData.data;
if (!data.data.length) { if (!data.data.length && !prevData.length) {
update = false; update = false;
} else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) { } else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) {
const prevTs = prevData[0][0]; const prevTs = prevData[0][0];
@ -1560,6 +1560,8 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
this.notifyDataLoaded(); this.notifyDataLoaded();
this.onDataUpdated(detectChanges); this.onDataUpdated(detectChanges);
} else if (this.loadingData) {
this.notifyDataLoaded();
} }
} }

12
ui-ngx/src/app/core/http/entity.service.ts

@ -41,7 +41,7 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models'; import { AliasInfo, StateParams, SubscriptionInfo } from '@core/api/widget-api.models';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@app/shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, DeprecatedFilter, KeyInfo } from '@app/shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { import {
AliasFilterType, AliasFilterType,
@ -65,7 +65,9 @@ import { Device, DeviceCredentialsType } from '@shared/models/device.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { import {
AlarmData, AlarmData,
AlarmDataQuery, AlarmFilter, AlarmFilterConfig, AlarmDataQuery,
AlarmFilter,
AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
EntityData, EntityData,
EntityDataQuery, EntityDataQuery,
@ -418,7 +420,11 @@ export class EntityService {
break; break;
case EntityType.WIDGETS_BUNDLE: case EntityType.WIDGETS_BUNDLE:
pageLink.sortOrder.property = 'title'; pageLink.sortOrder.property = 'title';
entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, config); entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, true, config);
break;
case EntityType.WIDGET_TYPE:
pageLink.sortOrder.property = 'name';
entitiesObservable = this.widgetService.getWidgetTypes(pageLink, true, false, DeprecatedFilter.ALL, null, config);
break; break;
case EntityType.NOTIFICATION_TARGET: case EntityType.NOTIFICATION_TARGET:
pageLink.sortOrder.property = 'name'; pageLink.sortOrder.property = 'name';

9
ui-ngx/src/app/core/http/widget.service.ts

@ -85,9 +85,12 @@ export class WidgetService {
); );
} }
public getWidgetBundles(pageLink: PageLink, fullSearch = false, config?: RequestConfig): Observable<PageData<WidgetsBundle>> { public getWidgetBundles(pageLink: PageLink, fullSearch = false,
return this.http.get<PageData<WidgetsBundle>>(`/api/widgetsBundles${pageLink.toQuery()}&fullSearch=${fullSearch}`, tenantOnly = false, config?: RequestConfig): Observable<PageData<WidgetsBundle>> {
defaultHttpOptionsFromConfig(config)); return this.http.get<PageData<WidgetsBundle>>(
`/api/widgetsBundles${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`,
defaultHttpOptionsFromConfig(config)
);
} }
public getWidgetsBundle(widgetsBundleId: string, public getWidgetsBundle(widgetsBundleId: string,

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

@ -817,3 +817,7 @@ export const getOS = (): string => {
export const camelCase = (str: string): string => { export const camelCase = (str: string): string => {
return _.camelCase(str); return _.camelCase(str);
}; };
export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => {
return _.mapKeys(obj, (value, key) => _.camelCase(key));
};

3
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss

@ -40,6 +40,9 @@
tb-entity-subtype-list { tb-entity-subtype-list {
flex: 1; flex: 1;
width: 180px; width: 180px;
.mdc-evolution-chip-set__chips {
width: 100%;
}
} }
.mat-mdc-chip { .mat-mdc-chip {

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

@ -319,9 +319,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const unacknowledgedAlarms = alarms.filter(alarm => { const unacknowledgedAlarms = alarms.filter(alarm => !alarm.acknowledged);
return alarm.status === AlarmStatus.CLEARED_UNACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
let title = ''; let title = '';
let content = ''; let content = '';
if (!unacknowledgedAlarms.length) { if (!unacknowledgedAlarms.length) {
@ -356,9 +354,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
const activeAlarms = alarms.filter(alarm => { const activeAlarms = alarms.filter(alarm => !alarm.cleared);
return alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK;
})
let title = ''; let title = '';
let content = ''; let content = '';
if (!activeAlarms.length) { if (!activeAlarms.length) {

4
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -75,14 +75,14 @@
<button *ngIf="!previewMode" <button *ngIf="!previewMode"
mat-button color="primary" mat-button color="primary"
type="button" type="button"
[disabled]="widgetFormGroup.invalid" [disabled]="!widgetFormGroup.valid"
(click)="previewMode = true" (click)="previewMode = true"
cdkFocusInitial> cdkFocusInitial>
{{ 'widget-config.preview' | translate }} {{ 'widget-config.preview' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="add()" (click)="add()"
[disabled]="widgetFormGroup.invalid"> [disabled]="!widgetFormGroup.valid">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
</div> </div>

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" <div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" tb-toast
fxFlex tb-fullscreen [fullscreenElement]="elRef.nativeElement" [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> fxFlex tb-fullscreen [fullscreenElement]="elRef.nativeElement" [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-dashboard-toolbar" <section class="tb-dashboard-toolbar"

12
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -125,7 +125,11 @@ div.tb-dashboard-page {
mat-drawer-container.tb-dashboard-layouts { mat-drawer-container.tb-dashboard-layouts {
width: 100%; width: 100%;
&.tb-shrinked { &.tb-shrinked {
width: 40%; width: max(40%, calc(100% - 956px));
@media screen and (min-width: 2000px) {
width: max(40%, calc(100% - 1192px));
}
} }
} }
@ -135,11 +139,11 @@ div.tb-dashboard-page {
} }
@media #{$mat-gt-md} { @media #{$mat-gt-md} {
width: 75% !important; width: min(85%, 956px) !important;
} }
@media #{$mat-gt-xl} { @media screen and (min-width: 2000px) {
width: 60% !important; width: min(60%, 1192px) !important;
} }
} }
} }

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -142,8 +142,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
columns: 2, columns: 2,
breakpoints: { breakpoints: {
'screen and (min-width: 2000px)': 5, 'screen and (min-width: 2000px)': 5,
'screen and (min-width: 1280px)': 4, 'screen and (min-width: 1097px)': 4,
'screen and (min-width: 600px)': 3 'gt-sm': 3,
'screen and (min-width: 721px)': 4,
'screen and (min-width: 485px)': 3
} }
}; };

6
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -34,7 +34,7 @@
</button> </button>
<button *ngIf="!previewMode" <button *ngIf="!previewMode"
fxHide.gt-sm fxHide.gt-sm
[disabled]="widgetFormGroup.invalid" [disabled]="!widgetFormGroup.valid"
mat-button color="primary" mat-button color="primary"
(click)="previewMode = true"> (click)="previewMode = true">
<mat-icon>visibility</mat-icon> <mat-icon>visibility</mat-icon>
@ -44,7 +44,7 @@
<div fxFlex.lt-md class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="12px"> <div fxFlex.lt-md class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="12px">
<button *ngIf="!previewMode" <button *ngIf="!previewMode"
fxHide.lt-md fxHide.lt-md
[disabled]="widgetFormGroup.invalid" [disabled]="!widgetFormGroup.valid"
mat-button color="primary" mat-button color="primary"
(click)="previewMode = true"> (click)="previewMode = true">
<mat-icon>visibility</mat-icon> <mat-icon>visibility</mat-icon>
@ -57,7 +57,7 @@
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
{{ 'action.decline' | translate }} {{ 'action.decline' | translate }}
</button> </button>
<button [disabled]="widgetFormGroup.invalid || !widgetFormGroup.dirty" <button [disabled]="!widgetFormGroup.valid || !widgetFormGroup.dirty"
mat-flat-button color="primary" mat-flat-button color="primary"
(click)="onApplyWidgetConfig()"> (click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon> <mat-icon>done</mat-icon>

86
ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html

@ -15,50 +15,52 @@
limitations under the License. limitations under the License.
--> -->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()"> <form fxLayout="column" class="tb-filter" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()">
<ng-container *ngFor="let column of showColumns"> <div fxLayout="column" class="tb-filter-container mat-padding">
<ng-container [ngSwitch]="column.key"> <ng-container *ngFor="let column of showColumns">
<ng-template [ngSwitchCase]="isSelector(column.key)"> <ng-container [ngSwitch]="column.key">
<mat-form-field> <ng-template [ngSwitchCase]="isSelector(column.key)">
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field>
<mat-select [formControlName]="column.key"> <mat-label>{{ column.title | translate}}</mat-label>
<mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option> <mat-select [formControlName]="column.key">
<mat-option *ngFor="let value of selectorValues(column.key)" [value]="value"> <mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option>
{{ value }} <mat-option *ngFor="let value of selectorValues(column.key)" [value]="value">
</mat-option> {{ value }}
</mat-select> </mat-option>
</mat-form-field> </mat-select>
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="isNumberFields(column.key)"> </ng-template>
<mat-form-field> <ng-template [ngSwitchCase]="isNumberFields(column.key)">
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field>
<input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key"> <mat-label>{{ column.title | translate}}</mat-label>
<mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')"> <input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key">
{{ 'event.min-value' | translate }} <mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')">
</mat-error> {{ 'event.min-value' | translate }}
</mat-form-field> </mat-error>
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="'isError'"> </ng-template>
<tb-checkbox formControlName="isError" [falseValue]="''" <ng-template [ngSwitchCase]="'isError'">
(ngModelChange)="changeIsError($event)"> <tb-checkbox formControlName="isError" [falseValue]="''"
{{ 'event.has-error' | translate }} (ngModelChange)="changeIsError($event)">
</tb-checkbox> {{ 'event.has-error' | translate }}
</ng-template> </tb-checkbox>
<ng-template [ngSwitchCase]="'errorStr'"> </ng-template>
<mat-form-field fxHide [fxShow]="showErrorMsgFields()"> <ng-template [ngSwitchCase]="'errorStr'">
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field fxHide [fxShow]="showErrorMsgFields()">
<input matInput type="text" name="errorSearchText" formControlName="errorStr"> <mat-label>{{ column.title | translate}}</mat-label>
</mat-form-field> <input matInput type="text" name="errorSearchText" formControlName="errorStr">
</ng-template> </mat-form-field>
<ng-container *ngSwitchDefault> </ng-template>
<mat-form-field> <ng-container *ngSwitchDefault>
<mat-label>{{ column.title | translate}}</mat-label> <mat-form-field>
<input matInput type="text" [name]="column.key" [formControlName]="column.key"> <mat-label>{{ column.title | translate}}</mat-label>
</mat-form-field> <input matInput type="text" [name]="column.key" [formControlName]="column.key">
</mat-form-field>
</ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </div>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> <div fxLayout="row" class="tb-panel-actions mat-padding" fxLayoutAlign="end center">
<button type="button" <button type="button"
mat-button mat-button
(click)="cancel()"> (click)="cancel()">

19
ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.scss

@ -16,20 +16,11 @@
:host { :host {
width: 100%; width: 100%;
min-width: 300px; min-width: 300px;
overflow: auto;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content { .tb-filter {
overflow: hidden; height: 100%;
background-color: #fff; &-container {
} overflow: auto;
}
.mat-padding {
padding: 16px;
} }
} }

30
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -30,7 +30,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EventService } from '@app/core/http/event.service'; import { EventService } from '@app/core/http/event.service';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component'; import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EntityTypeResource } from '@shared/models/entity-type.models'; import { EntityTypeResource } from '@shared/models/entity-type.models';
import { Observable } from 'rxjs'; import { fromEvent, Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
@ -50,6 +50,7 @@ import {
EventFilterPanelData, EventFilterPanelData,
FilterEntityColumn FilterEntityColumn
} from '@home/components/event/event-filter-panel.component'; } from '@home/components/event/event-filter-panel.component';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> { export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
@ -457,19 +458,16 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
$event.stopPropagation(); $event.stopPropagation();
} }
const target = $event.target || $event.srcElement || $event.currentTarget; const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig({
config.backdropClass = 'cdk-overlay-transparent-backdrop'; panelClass: 'tb-panel-container',
config.hasBackdrop = true; backdropClass: 'cdk-overlay-transparent-backdrop',
const connectedPosition: ConnectedPosition = { hasBackdrop: true,
originX: 'end', height: 'fit-content',
originY: 'bottom', maxHeight: '65vh'
overlayX: 'end', });
overlayY: 'top' config.positionStrategy = this.overlay.position()
}; .flexibleConnectedTo(target as HTMLElement)
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions(DEFAULT_OVERLAY_POSITIONS);
.withPositions([connectedPosition]);
config.maxHeight = '70vh';
config.height = 'min-content';
const overlayRef = this.overlay.create(config); const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => { overlayRef.backdropClick().subscribe(() => {
@ -491,7 +489,11 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent, const componentRef = overlayRef.attach(new ComponentPortal(EventFilterPanelComponent,
this.viewContainerRef, injector)); this.viewContainerRef, injector));
const resizeWindows$ = fromEvent(window, 'resize').subscribe(() => {
overlayRef.updatePosition();
});
componentRef.onDestroy(() => { componentRef.onDestroy(() => {
resizeWindows$.unsubscribe();
if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) { if (componentRef.instance.result && !isEqual(this.filterParams, componentRef.instance.result.filterParams)) {
this.filterParams = componentRef.instance.result.filterParams; this.filterParams = componentRef.instance.result.filterParams;
this.getTable().paginator.pageIndex = 0; this.getTable().paginator.pageIndex = 0;

2
ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts

@ -47,7 +47,7 @@ export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChang
viewport: CdkVirtualScrollViewport; viewport: CdkVirtualScrollViewport;
@Input() @Input()
columns: ScrollGridColumns = {columns: 1}; columns: ScrollGridColumns | number = 1;
@Input() @Input()
fetchFunction: GridEntitiesFetchFunction<T, F>; fetchFunction: GridEntitiesFetchFunction<T, F>;

4
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.scss

@ -40,12 +40,10 @@
padding: 0 !important; padding: 0 !important;
.mat-stepper-horizontal { .mat-stepper-horizontal {
display: flex;
height: 100%;
overflow: hidden; overflow: hidden;
.mat-horizontal-stepper-wrapper { .mat-horizontal-stepper-wrapper {
flex: 1 1 100%; max-height: 100%;
} }
.mat-horizontal-content-container { .mat-horizontal-content-container {

2
ui-ngx/src/app/modules/home/components/public-api.ts

@ -15,3 +15,5 @@
/// ///
export * from './home-components.module'; export * from './home-components.module';
export * from './widget/config/widget-config.component.models';

1
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarms-table-basic-config.component.html

@ -40,6 +40,7 @@
keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}" keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}" removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}" noKeysText="{{ 'widgets.table.no-columns' | translate }}"
requiredKeysText="{{ 'widgets.table.alarm-column-error' | translate }}"
hideDataKeyColor hideDataKeyColor
hideUnits hideUnits
hideDecimals hideDecimals

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

@ -82,6 +82,9 @@ import {
import { import {
LiquidLevelCardBasicConfigComponent LiquidLevelCardBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component'; } from '@home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component';
import {
DoughnutBasicConfigComponent
} from '@home/components/widget/config/basic/chart/doughnut-basic-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -107,7 +110,8 @@ import {
RadialGaugeBasicConfigComponent, RadialGaugeBasicConfigComponent,
ThermometerScaleGaugeBasicConfigComponent, ThermometerScaleGaugeBasicConfigComponent,
CompassGaugeBasicConfigComponent, CompassGaugeBasicConfigComponent,
LiquidLevelCardBasicConfigComponent LiquidLevelCardBasicConfigComponent,
DoughnutBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -137,7 +141,8 @@ import {
RadialGaugeBasicConfigComponent, RadialGaugeBasicConfigComponent,
ThermometerScaleGaugeBasicConfigComponent, ThermometerScaleGaugeBasicConfigComponent,
CompassGaugeBasicConfigComponent, CompassGaugeBasicConfigComponent,
LiquidLevelCardBasicConfigComponent LiquidLevelCardBasicConfigComponent,
DoughnutBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -161,5 +166,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-radial-gauge-basic-config': RadialGaugeBasicConfigComponent, 'tb-radial-gauge-basic-config': RadialGaugeBasicConfigComponent,
'tb-thermometer-scale-gauge-basic-config': ThermometerScaleGaugeBasicConfigComponent, 'tb-thermometer-scale-gauge-basic-config': ThermometerScaleGaugeBasicConfigComponent,
'tb-compass-gauge-basic-config': CompassGaugeBasicConfigComponent, 'tb-compass-gauge-basic-config': CompassGaugeBasicConfigComponent,
'tb-liquid-level-card-basic-config': LiquidLevelCardBasicConfigComponent 'tb-liquid-level-card-basic-config': LiquidLevelCardBasicConfigComponent,
'tb-doughnut-basic-config': DoughnutBasicConfigComponent
}; };

5
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.html

@ -31,7 +31,10 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1" <tb-image-cards-select rowHeight="2:1"
cols="2" [cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout"> label="{{ 'widgets.progress-bar.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of progressBarLayouts" <tb-image-cards-select-option *ngFor="let layout of progressBarLayouts"
[value]="layout" [value]="layout"

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

@ -31,8 +31,10 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}" <tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}"
[cols]="horizontal ? 2 : 4" [cols]="{columns: horizontal ? 2 : 4,
[colsLtMd]="horizontal ? 1 : 2" breakpoints: {
'lt-md': horizontal ? 1 : 2
}}"
label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout"> label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueCardLayouts" <tb-image-cards-select-option *ngFor="let layout of valueCardLayouts"
[value]="layout" [value]="layout"

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

@ -30,7 +30,10 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="2:1" <tb-image-cards-select rowHeight="2:1"
cols="2" [cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout"> label="{{ 'widgets.value-chart-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueChartCardLayouts" <tb-image-cards-select-option *ngFor="let layout of valueChartCardLayouts"
[value]="layout" [value]="layout"

254
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html

@ -0,0 +1,254 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="doughnutWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeys
forceSingleDatasource
formControlName="datasources">
</tb-datasources>
<tb-data-keys-panel
panelTitle="{{ 'widgets.chart.series' | translate }}"
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}"
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
hideUnits
hideDecimals
hideDataKeyUnits
hideDataKeyDecimals
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '8:5' : '5:4' }}"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.doughnut.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of doughnutLayouts"
[value]="layout"
[image]="doughnutLayoutImageMap.get(layout)">
{{ doughnutLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.doughnut.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="doughnutWidgetConfigForm.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 space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
iconClearButton
[color]="doughnutWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="clockwise">
{{ 'widgets.doughnut.clockwise-layout' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sortSeries">
{{ 'widgets.doughnut.sort-series' | translate }}
</mat-slide-toggle>
</div>
<div [fxShow]="totalEnabled" class="tb-form-row space-between">
<div>{{ 'widgets.doughnut.central-total-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="totalValueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="totalValueColor" settingsKey="{{'widgets.doughnut.central-total-value' | translate }}">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-unit-input
formControlName="units">
</tb-unit-input>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="doughnutWidgetConfigForm.get('showLegend').value" [disabled]="!doughnutWidgetConfigForm.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'legend.position' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="legendPosition">
<mat-option *ngFor="let pos of doughnutLegendPositions" [value]="pos">
{{ doughnutLegendPositionTranslationMap.get(pos) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.doughnut.legend-label' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendLabelFont"
previewText="Wind power">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendLabelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.doughnut.legend-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendValueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendValueColor">
</tb-color-input>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="doughnutWidgetConfigForm.get('showTooltip').value" [disabled]="!doughnutWidgetConfigForm.get('showTooltip').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.doughnut.tooltip' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.doughnut.tooltip-value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tooltipValueType">
<mat-option *ngFor="let type of doughnutTooltipValueTypes" [value]="type">
{{ doughnutTooltipValueTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipValueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="tooltipValueFont"
[previewText]="tooltipValuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.doughnut.tooltip-background-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipBackgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.doughnut.tooltip-background-blur' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-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="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>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

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

@ -0,0 +1,341 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } 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 {
DataKey,
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import {
doughnutDefaultSettings,
DoughnutLayout,
doughnutLayoutImages,
doughnutLayouts,
doughnutLayoutTranslations,
doughnutLegendPositions,
doughnutLegendPositionTranslations,
DoughnutTooltipValueType,
doughnutTooltipValueTypes,
doughnutTooltipValueTypeTranslations,
DoughnutWidgetSettings,
horizontalDoughnutLayoutImages
} from '@home/components/widget/lib/chart/doughnut-widget.models';
@Component({
selector: 'tb-doughnut-basic-config',
templateUrl: './doughnut-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.doughnutWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get displayTimewindowConfig(): boolean {
const datasources = this.doughnutWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.doughnutWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
doughnutLayouts = doughnutLayouts;
doughnutLayoutTranslationMap = doughnutLayoutTranslations;
horizontal = false;
doughnutLayoutImageMap: Map<DoughnutLayout, string>;
doughnutLegendPositions = doughnutLegendPositions;
doughnutLegendPositionTranslationMap = doughnutLegendPositionTranslations;
doughnutTooltipValueTypes = doughnutTooltipValueTypes;
doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations;
doughnutWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
get totalEnabled(): boolean {
const layout: DoughnutLayout = this.doughnutWidgetConfigForm.get('layout').value;
return layout === DoughnutLayout.with_total;
}
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.doughnutWidgetConfigForm;
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.doughnutLayoutImageMap = this.horizontal ? horizontalDoughnutLayoutImages : doughnutLayoutImages;
super.setupConfig(widgetConfig);
}
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] {
return [{ name: 'windPower', label: 'Wind power', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#08872B' },
{ name: 'solarPower', label: 'Solar power', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FF4D5A' }];
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: DoughnutWidgetSettings = {...doughnutDefaultSettings(this.horizontal), ...(configData.config.settings || {})};
this.doughnutWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
series: [this.getSeries(configData.config.datasources), []],
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
clockwise: [settings.clockwise, []],
sortSeries: [settings.sortSeries, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
totalValueFont: [settings.totalValueFont, []],
totalValueColor: [settings.totalValueColor, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
showLegend: [settings.showLegend, []],
legendPosition: [settings.legendPosition, []],
legendLabelFont: [settings.legendLabelFont, []],
legendLabelColor: [settings.legendLabelColor, []],
legendValueFont: [settings.legendValueFont, []],
legendValueColor: [settings.legendValueColor, []],
showTooltip: [settings.showTooltip, []],
tooltipValueType: [settings.tooltipValueType, []],
tooltipValueDecimals: [settings.tooltipValueDecimals, []],
tooltipValueFont: [settings.tooltipValueFont, []],
tooltipValueColor: [settings.tooltipValueColor, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources);
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.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.clockwise = config.clockwise;
this.widgetConfig.config.settings.sortSeries = config.sortSeries;
this.widgetConfig.config.settings.totalValueFont = config.totalValueFont;
this.widgetConfig.config.settings.totalValueColor = config.totalValueColor;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.settings.showLegend = config.showLegend;
this.widgetConfig.config.settings.legendPosition = config.legendPosition;
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont;
this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor;
this.widgetConfig.config.settings.legendValueFont = config.legendValueFont;
this.widgetConfig.config.settings.legendValueColor = config.legendValueColor;
this.widgetConfig.config.settings.showTooltip = config.showTooltip;
this.widgetConfig.config.settings.tooltipValueType = config.tooltipValueType;
this.widgetConfig.config.settings.tooltipValueDecimals = config.tooltipValueDecimals;
this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont;
this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor;
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['layout', 'showTitle', 'showTitleIcon', 'showLegend', 'showTooltip'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const layout: DoughnutLayout = this.doughnutWidgetConfigForm.get('layout').value;
const showTitle: boolean = this.doughnutWidgetConfigForm.get('showTitle').value;
const showTitleIcon: boolean = this.doughnutWidgetConfigForm.get('showTitleIcon').value;
const showLegend: boolean = this.doughnutWidgetConfigForm.get('showLegend').value;
const showTooltip: boolean = this.doughnutWidgetConfigForm.get('showTooltip').value;
const totalEnabled = layout === DoughnutLayout.with_total;
if (showTitle) {
this.doughnutWidgetConfigForm.get('title').enable();
this.doughnutWidgetConfigForm.get('titleFont').enable();
this.doughnutWidgetConfigForm.get('titleColor').enable();
this.doughnutWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false});
if (showTitleIcon) {
this.doughnutWidgetConfigForm.get('titleIcon').enable();
this.doughnutWidgetConfigForm.get('iconColor').enable();
} else {
this.doughnutWidgetConfigForm.get('titleIcon').disable();
this.doughnutWidgetConfigForm.get('iconColor').disable();
}
} else {
this.doughnutWidgetConfigForm.get('title').disable();
this.doughnutWidgetConfigForm.get('titleFont').disable();
this.doughnutWidgetConfigForm.get('titleColor').disable();
this.doughnutWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false});
this.doughnutWidgetConfigForm.get('titleIcon').disable();
this.doughnutWidgetConfigForm.get('iconColor').disable();
}
if (showLegend) {
this.doughnutWidgetConfigForm.get('legendPosition').enable();
this.doughnutWidgetConfigForm.get('legendLabelFont').enable();
this.doughnutWidgetConfigForm.get('legendLabelColor').enable();
this.doughnutWidgetConfigForm.get('legendValueFont').enable();
this.doughnutWidgetConfigForm.get('legendValueColor').enable();
} else {
this.doughnutWidgetConfigForm.get('legendPosition').disable();
this.doughnutWidgetConfigForm.get('legendLabelFont').disable();
this.doughnutWidgetConfigForm.get('legendLabelColor').disable();
this.doughnutWidgetConfigForm.get('legendValueFont').disable();
this.doughnutWidgetConfigForm.get('legendValueColor').disable();
}
if (showTooltip) {
this.doughnutWidgetConfigForm.get('tooltipValueType').enable();
this.doughnutWidgetConfigForm.get('tooltipValueDecimals').enable();
this.doughnutWidgetConfigForm.get('tooltipValueFont').enable();
this.doughnutWidgetConfigForm.get('tooltipValueColor').enable();
this.doughnutWidgetConfigForm.get('tooltipBackgroundColor').enable();
this.doughnutWidgetConfigForm.get('tooltipBackgroundBlur').enable();
} else {
this.doughnutWidgetConfigForm.get('tooltipValueType').disable();
this.doughnutWidgetConfigForm.get('tooltipValueDecimals').disable();
this.doughnutWidgetConfigForm.get('tooltipValueFont').disable();
this.doughnutWidgetConfigForm.get('tooltipValueColor').disable();
this.doughnutWidgetConfigForm.get('tooltipBackgroundColor').disable();
this.doughnutWidgetConfigForm.get('tooltipBackgroundBlur').disable();
}
if (totalEnabled) {
this.doughnutWidgetConfigForm.get('totalValueFont').enable();
this.doughnutWidgetConfigForm.get('totalValueColor').enable();
} else {
this.doughnutWidgetConfigForm.get('totalValueFont').disable();
this.doughnutWidgetConfigForm.get('totalValueColor').disable();
}
}
private getSeries(datasources?: Datasource[]): DataKey[] {
if (datasources && datasources.length) {
return datasources[0].dataKeys || [];
}
return [];
}
private setSeries(series: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
datasources[0].dataKeys = series;
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.doughnutWidgetConfigForm.get('units').value;
const decimals: number = this.doughnutWidgetConfigForm.get('decimals').value;
return formatValue(110, decimals, units, false);
}
private _tooltipValuePreviewFn(): string {
const tooltipValueType: DoughnutTooltipValueType = this.doughnutWidgetConfigForm.get('tooltipValueType').value;
const decimals: number = this.doughnutWidgetConfigForm.get('tooltipValueDecimals').value;
if (tooltipValueType === DoughnutTooltipValueType.percentage) {
return formatValue(35, decimals, '%', false);
} else {
const units: string = this.doughnutWidgetConfigForm.get('units').value;
return formatValue(110, decimals, units, false);
}
}
}

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

@ -69,9 +69,11 @@ import { coerceBoolean } from '@shared/decorators/coercion';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name;
export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => { export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => {
const dataKey: DataKey = control.value; const dataKey: DataKey = control.value;
if (!dataKey || !dataKey.type || !dataKey.name) { if (!dataKeyValid(dataKey)) {
return { return {
dataKey: true dataKey: true
}; };

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

@ -44,6 +44,8 @@
[hideDataKeyColor]="hideDataKeyColor" [hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals" [hideDecimals]="hideDecimals"
[hideUnits]="hideUnits" [hideUnits]="hideUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals"
[hideDataKeyUnits]="hideDataKeyUnits"
[dataKeyType]="dataKeyType" [dataKeyType]="dataKeyType"
[singleRow]="false" [singleRow]="false"
[keySettingsTitle]="keySettingsTitle" [keySettingsTitle]="keySettingsTitle"

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

@ -38,7 +38,7 @@ import {
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models';
import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component'; import { dataKeyRowValidator, dataKeyValid } from '@home/components/widget/config/basic/common/data-key-row.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
@ -107,6 +107,14 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean() @coerceBoolean()
hideDecimals = false; hideDecimals = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
hideSourceSelection = false; hideSourceSelection = false;
@ -160,7 +168,13 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
keys: [this.fb.array([]), []] keys: [this.fb.array([]), []]
}); });
this.keysListFormGroup.valueChanges.subscribe( this.keysListFormGroup.valueChanges.subscribe(
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value) () => {
let keys: DataKey[] = this.keysListFormGroup.get('keys').value;
if (keys) {
keys = keys.filter(k => dataKeyValid(k));
}
this.propagateChange(keys);
}
); );
this.updateParams(); this.updateParams();
} }

2
ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/analog-gauge-basic-config.component.ts

@ -131,7 +131,7 @@ export class GaugeBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.settings.defaultColor = config.defaultColor; this.widgetConfig.config.settings.defaultColor = config.defaultColor;
this.widgetConfig.config.settings.colorPlate = config.colorPlate; this.widgetConfig.config.settings.colorPlate = config.colorPlate;
this.widgetConfig.config.settings.highlights = config.highlights.rangeList; this.widgetConfig.config.settings.highlights = config.highlights;
return this.widgetConfig; return this.widgetConfig;
} }

6
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html

@ -31,8 +31,10 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="1:1" <tb-image-cards-select rowHeight="1:1"
cols="4" [cols]="{columns: 4,
colsLtMd="2" breakpoints: {
'lt-md': 2
}}"
label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout"> label="{{ 'widgets.battery-level.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of batteryLevelLayouts" <tb-image-cards-select-option *ngFor="let layout of batteryLevelLayouts"
[value]="layout" [value]="layout"

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

Loading…
Cancel
Save