Browse Source

Merge branch 'improvement/gateway-dashboard' into fix/3903-username-field-static-height

pull/11057/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
d63168ceeb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/pom.xml
  2. 14
      application/src/main/data/json/edge/instructions/install/centos/instructions.md
  3. 14
      application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md
  4. 197
      application/src/main/data/json/tenant/dashboards/gateways.json
  5. 6
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  6. 65
      application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java
  7. 71
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java
  8. 55
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
  9. 29
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  10. 2
      common/actor/pom.xml
  11. 2
      common/cache/pom.xml
  12. 2
      common/cluster-api/pom.xml
  13. 2
      common/coap-server/pom.xml
  14. 2
      common/dao-api/pom.xml
  15. 2
      common/data/pom.xml
  16. 6
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  17. 29
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  18. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java
  19. 2
      common/edge-api/pom.xml
  20. 2
      common/message/pom.xml
  21. 2
      common/pom.xml
  22. 2
      common/proto/pom.xml
  23. 2
      common/queue/pom.xml
  24. 2
      common/script/pom.xml
  25. 2
      common/script/remote-js-client/pom.xml
  26. 2
      common/script/script-api/pom.xml
  27. 2
      common/stats/pom.xml
  28. 2
      common/transport/coap/pom.xml
  29. 2
      common/transport/http/pom.xml
  30. 2
      common/transport/lwm2m/pom.xml
  31. 2
      common/transport/mqtt/pom.xml
  32. 2
      common/transport/pom.xml
  33. 2
      common/transport/snmp/pom.xml
  34. 2
      common/transport/transport-api/pom.xml
  35. 2
      common/util/pom.xml
  36. 2
      common/version-control/pom.xml
  37. 2
      dao/pom.xml
  38. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java
  39. 7
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  40. 52
      dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java
  41. 16
      dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java
  42. 2
      monitoring/pom.xml
  43. 91
      monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java
  44. 2
      msa/black-box-tests/pom.xml
  45. 2
      msa/js-executor/package.json
  46. 2
      msa/js-executor/pom.xml
  47. 2
      msa/monitoring/pom.xml
  48. 2
      msa/pom.xml
  49. 2
      msa/tb-node/pom.xml
  50. 2
      msa/tb/pom.xml
  51. 2
      msa/transport/coap/pom.xml
  52. 2
      msa/transport/http/pom.xml
  53. 2
      msa/transport/lwm2m/pom.xml
  54. 2
      msa/transport/mqtt/pom.xml
  55. 2
      msa/transport/pom.xml
  56. 2
      msa/transport/snmp/pom.xml
  57. 2
      msa/vc-executor-docker/pom.xml
  58. 2
      msa/vc-executor/pom.xml
  59. 2
      msa/web-ui/package.json
  60. 2
      msa/web-ui/pom.xml
  61. 4
      netty-mqtt/pom.xml
  62. 2
      pom.xml
  63. 2
      rest-client/pom.xml
  64. 2
      rule-engine/pom.xml
  65. 2
      rule-engine/rule-engine-api/pom.xml
  66. 2
      rule-engine/rule-engine-components/pom.xml
  67. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  68. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  69. 64
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
  70. 87
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java
  71. 421
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java
  72. 2
      tools/pom.xml
  73. 2
      transport/coap/pom.xml
  74. 2
      transport/http/pom.xml
  75. 2
      transport/lwm2m/pom.xml
  76. 2
      transport/mqtt/pom.xml
  77. 2
      transport/pom.xml
  78. 2
      transport/snmp/pom.xml
  79. 2
      ui-ngx/package.json
  80. 2
      ui-ngx/pom.xml
  81. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html
  82. 65
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.html
  83. 29
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.scss
  84. 132
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.ts
  85. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html
  86. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  87. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  88. 1113
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  89. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  90. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  91. 104
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  92. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  93. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  94. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html
  95. 15
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss
  96. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts
  97. 36
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  98. 55
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  99. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  100. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>application</artifactId> <artifactId>application</artifactId>

14
application/src/main/data/json/edge/instructions/install/centos/instructions.md

@ -8,15 +8,15 @@ sudo yum install -y nano wget
sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
``` ```
#### Install Java 11 (OpenJDK) #### Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11: ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17:
```bash ```bash
sudo yum install java-11-openjdk sudo yum install java-17-openjdk
{:copy-code} {:copy-code}
``` ```
Please don't forget to configure your operating system to use OpenJDK 11 by default. Please don't forget to configure your operating system to use OpenJDK 17 by default.
You can configure which version is the default using the following command: You can configure which version is the default using the following command:
```bash ```bash
@ -34,7 +34,7 @@ java -version
Expected command output is: Expected command output is:
```text ```text
openjdk version "11.0.xx" openjdk version "17.x.xx"
OpenJDK Runtime Environment (...) OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...) OpenJDK 64-Bit Server VM (build ...)
``` ```
@ -144,14 +144,14 @@ CREATE DATABASE tb_edge;
Download installation package: Download installation package:
```bash ```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.rpm wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code} {:copy-code}
``` ```
Go to the download repository and install ThingsBoard Edge service: Go to the download repository and install ThingsBoard Edge service:
```bash ```bash
sudo rpm -Uvh tb-edge-${TB_EDGE_VERSION}.rpm sudo rpm -Uvh tb-edge-${TB_EDGE_TAG}.rpm
{:copy-code} {:copy-code}
``` ```

14
application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md

@ -1,15 +1,15 @@
Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. Here is the list of commands, that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server.
#### Install Java 11 (OpenJDK) #### Install Java 17 (OpenJDK)
ThingsBoard service is running on Java 11. Follow these instructions to install OpenJDK 11: ThingsBoard service is running on Java 17. Follow these instructions to install OpenJDK 17:
```bash ```bash
sudo apt update sudo apt update
sudo apt install openjdk-11-jdk sudo apt install openjdk-17-jdk
{:copy-code} {:copy-code}
``` ```
Please don't forget to configure your operating system to use OpenJDK 11 by default. Please don't forget to configure your operating system to use OpenJDK 17 by default.
You can configure which version is the default using the following command: You can configure which version is the default using the following command:
```bash ```bash
@ -27,7 +27,7 @@ java -version
Expected command output is: Expected command output is:
```text ```text
openjdk version "11.0.xx" openjdk version "17.x.xx"
OpenJDK Runtime Environment (...) OpenJDK Runtime Environment (...)
OpenJDK 64-Bit Server VM (build ...) OpenJDK 64-Bit Server VM (build ...)
``` ```
@ -76,14 +76,14 @@ CREATE DATABASE tb_edge;
Download installation package: Download installation package:
```bash ```bash
wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_VERSION}/tb-edge-${TB_EDGE_VERSION}.deb wget https://github.com/thingsboard/thingsboard-edge/releases/download/v${TB_EDGE_TAG}/tb-edge-${TB_EDGE_TAG}.deb
{:copy-code} {:copy-code}
``` ```
Go to the download repository and install ThingsBoard Edge service: Go to the download repository and install ThingsBoard Edge service:
```bash ```bash
sudo dpkg -i tb-edge-${TB_EDGE_VERSION}.deb sudo dpkg -i tb-edge-${TB_EDGE_TAG}.deb
{:copy-code} {:copy-code}
``` ```

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

@ -248,7 +248,7 @@
"type": "customPretty", "type": "customPretty",
"customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\r\n (ngSubmit)=\"save($event)\" class=\"add-entity-form\">\r\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\r\n <h2>Add gateway</h2>\r\n <span fxFlex></span>\r\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\r\n <mat-icon class=\"material-icons\">close</mat-icon>\r\n </button>\r\n </mat-toolbar>\r\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\r\n </mat-progress-bar>\r\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\r\n <div mat-dialog-content fxLayout=\"column\">\r\n <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\" fxLayoutGap.xs=\"0\">\r\n <mat-form-field fxFlex class=\"mat-block\">\r\n <mat-label>Name</mat-label>\r\n <input matInput formControlName=\"entityName\" required>\r\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\r\n Gateway name is required.\r\n </mat-error>\r\n </mat-form-field>\r\n </div>\r\n <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\" fxLayoutGap.xs=\"0\">\r\n <tb-entity-subtype-autocomplete\r\n fxFlex\r\n class=\"mat-block\"\r\n formControlName=\"type\"\r\n [required]=\"true\"\r\n [entityType]=\"'DEVICE'\"\r\n ></tb-entity-subtype-autocomplete>\r\n </div>\r\n </div>\r\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\r\n <button mat-button color=\"primary\"\r\n type=\"button\"\r\n [disabled]=\"(isLoading$ | async)\"\r\n (click)=\"cancel()\" cdkFocusInitial>\r\n Cancel\r\n </button>\r\n <button mat-button mat-raised-button color=\"primary\"\r\n type=\"submit\"\r\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\r\n Create\r\n </button>\r\n </div>\r\n</form>\r\n", "customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\r\n (ngSubmit)=\"save($event)\" class=\"add-entity-form\">\r\n <mat-toolbar fxLayout=\"row\" color=\"primary\">\r\n <h2>Add gateway</h2>\r\n <span fxFlex></span>\r\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\r\n <mat-icon class=\"material-icons\">close</mat-icon>\r\n </button>\r\n </mat-toolbar>\r\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\r\n </mat-progress-bar>\r\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\r\n <div mat-dialog-content fxLayout=\"column\">\r\n <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\" fxLayoutGap.xs=\"0\">\r\n <mat-form-field fxFlex class=\"mat-block\">\r\n <mat-label>Name</mat-label>\r\n <input matInput formControlName=\"entityName\" required>\r\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\r\n Gateway name is required.\r\n </mat-error>\r\n </mat-form-field>\r\n </div>\r\n <div fxLayout=\"row\" fxLayoutGap=\"8px\" fxLayout.xs=\"column\" fxLayoutGap.xs=\"0\">\r\n <tb-entity-subtype-autocomplete\r\n fxFlex\r\n class=\"mat-block\"\r\n formControlName=\"type\"\r\n [required]=\"true\"\r\n [entityType]=\"'DEVICE'\"\r\n ></tb-entity-subtype-autocomplete>\r\n </div>\r\n </div>\r\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\r\n <button mat-button color=\"primary\"\r\n type=\"button\"\r\n [disabled]=\"(isLoading$ | async)\"\r\n (click)=\"cancel()\" cdkFocusInitial>\r\n Cancel\r\n </button>\r\n <button mat-button mat-raised-button color=\"primary\"\r\n type=\"submit\"\r\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\r\n Create\r\n </button>\r\n </div>\r\n</form>\r\n",
"customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n", "customCss": ".add-entity-form {\r\n min-width: 400px !important;\r\n}\r\n\r\n.add-entity-form .boolean-value-input {\r\n padding-left: 5px;\r\n}\r\n\r\n.add-entity-form .boolean-value-input .checkbox-label {\r\n margin-bottom: 8px;\r\n color: rgba(0,0,0,0.54);\r\n font-size: 12px;\r\n}\r\n\r\n.relations-list .header {\r\n padding-right: 5px;\r\n padding-bottom: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .header .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n color: rgba(0, 0, 0, .54);\r\n white-space: nowrap;\r\n}\r\n\r\n.relations-list .mat-form-field-infix {\r\n width: auto !important;\r\n}\r\n\r\n.relations-list .body {\r\n padding-right: 5px;\r\n padding-bottom: 15px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .row {\r\n padding-top: 5px;\r\n}\r\n\r\n.relations-list .body .cell {\r\n padding-right: 5px;\r\n padding-left: 5px;\r\n}\r\n\r\n.relations-list .body .md-button {\r\n margin: 0;\r\n}\r\n\r\n",
"customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Docker commands\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n", "customFunction": "let $injector = widgetContext.$scope.$injector;\r\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\r\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\r\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\r\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\r\nlet entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService'));\r\nlet userSettingsService = $injector.get(widgetContext.servicesMap.get('userSettingsService'));\r\n\r\nopenAddEntityDialog();\r\n\r\nfunction openAddEntityDialog() {\r\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\r\n}\r\n\r\nfunction AddEntityDialogController(instance) {\r\n let vm = instance;\r\n let userSettings;\r\n userSettingsService.loadUserSettings().subscribe(settings=> {\r\n userSettings = settings;\r\n if (!userSettings.createdGatewaysCount) userSettings.createdGatewaysCount = 0;\r\n });\r\n \r\n\r\n vm.addEntityFormGroup = vm.fb.group({\r\n entityName: ['', [vm.validators.required]],\r\n entityType: ['DEVICE'],\r\n entityLabel: [''],\r\n type: ['', [vm.validators.required]],\r\n });\r\n\r\n vm.cancel = function() {\r\n vm.dialogRef.close(null);\r\n };\r\n\r\n\r\n vm.save = function($event) {\r\n vm.addEntityFormGroup.markAsPristine();\r\n saveEntityObservable().subscribe(\r\n function (device) {\r\n widgetContext.updateAliases();\r\n userSettingsService.putUserSettings({ createdGatewaysCount: ++userSettings.createdGatewaysCount }).subscribe(_=>{\r\n });\r\n vm.dialogRef.close(null);\r\n openCommandDialog(device, $event);\r\n }\r\n );\r\n };\r\n \r\n function openCommandDialog(device, $event) {\r\n vm.device = device;\r\n let openCommandAction = widgetContext.actionsApi.getActionDescriptors(\"actionCellButton\").find(action => action.name == \"Launch command\");\r\n widgetContext.actionsApi.handleWidgetAction($event, openCommandAction, device.id, device.name, {newDevice: true});\r\n goToConfigState();\r\n }\r\n\r\n \r\n function goToConfigState() {\r\n const stateParams = {};\r\n stateParams.entityId = vm.device.id;\r\n stateParams.entityName = vm.device.name;\r\n const newStateParams = {\r\n targetEntityParamName: 'default',\r\n new_gateway: {\r\n entityId: vm.device.id,\r\n entityName: vm.device.name\r\n }\r\n }\r\n const params = {...stateParams, ...newStateParams};\r\n widgetContext.stateController.openState('gateway_details', params, false);\r\n }\r\n\r\n function saveEntityObservable() {\r\n const formValues = vm.addEntityFormGroup.value;\r\n let entity = {\r\n name: formValues.entityName,\r\n type: formValues.type,\r\n label: formValues.entityLabel,\r\n additionalInfo: {\r\n gateway: true\r\n }\r\n };\r\n return deviceService.saveDevice(entity);\r\n }\r\n}\r\n",
"customResources": [], "customResources": [],
"openInSeparateDialog": false, "openInSeparateDialog": false,
"openInPopover": false, "openInPopover": false,
@ -2134,7 +2134,7 @@
"padding": "0px", "padding": "0px",
"settings": { "settings": {
"useMarkdownTextFunction": false, "useMarkdownTextFunction": false,
"markdownTextPattern": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\r\n <mat-tab-group [(selectedIndex)]=\"selectedTabIndex\">\r\n <mat-tab label=\"All\" value=\"gateway_devices_0\"></mat-tab>\r\n <mat-tab label=\"MQTT\" value=\"gateway_devices_1\"></mat-tab>\r\n <mat-tab label=\"MODBUS\" value=\"gateway_devices_2\"></mat-tab>\r\n <mat-tab label=\"GRPC\" value=\"gateway_devices_3\"></mat-tab>\r\n <mat-tab label=\"OPCUA\" value=\"gateway_devices_4\"> </mat-tab>\r\n <mat-tab label=\"OPCUA ASYNCIO\" value=\"gateway_devices_5\"></mat-tab>\r\n <mat-tab label=\"BLE\" value=\"gateway_devices_6\"></mat-tab>\r\n <mat-tab label=\"REQUEST\" value=\"gateway_devices_7\"></mat-tab>\r\n <mat-tab label=\"CAN\" value=\"gateway_devices_8\"></mat-tab>\r\n <mat-tab label=\"BACNET\" value=\"gateway_devices_9\"></mat-tab>\r\n <mat-tab label=\"ODBC\" value=\"gateway_devices_10\"></mat-tab>\r\n <mat-tab label=\"REST\" value=\"gateway_devices_11\"></mat-tab>\r\n <mat-tab label=\"SNMP\" value=\"gateway_devices_12\"></mat-tab>\r\n <mat-tab label=\"FTP\" value=\"gateway_devices_13\"></mat-tab>\r\n <mat-tab label=\"SOCKET\" value=\"gateway_devices_14\"></mat-tab>\r\n <mat-tab label=\"XMPP\" value=\"gateway_devices_15\"></mat-tab>\r\n <mat-tab label=\"OCCP\" value=\"gateway_devices_16\"></mat-tab>\r\n <mat-tab label=\"CUSTOM\" value=\"gateway_devices_17\"></mat-tab>\r\n </mat-tab-group><tb-dashboard-state *ngIf=\"selectedTabIndex == 1\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_1\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 2\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_2\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 3\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_3\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 4\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_4\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 5\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_5\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 6\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 7\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 8\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 9\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 10\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 11\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 12\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 13\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 14\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 15\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 16\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 17\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"!selectedTabIndex\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\r\n</div>\r\n", "markdownTextPattern": "<div style=\"width: 100%; height: 100%; padding: 0;\" fxFlex fxLayout=\"column\">\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 6\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_6\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 7\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_7\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 8\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_8\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 9\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_9\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 10\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_10\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 11\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_11\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 12\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_12\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 13\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_13\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 14\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_14\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 15\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_15\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 16\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_16\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"selectedTabIndex == 17\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_17\"></tb-dashboard-state>\r\n <tb-dashboard-state *ngIf=\"!selectedTabIndex\" [ctx]=\"ctx\" fxFlex syncParentStateParams=\"true\" stateId=\"gateway_devices_0\"></tb-dashboard-state>\r\n</div>\r\n",
"applyDefaultMarkdownStyle": false, "applyDefaultMarkdownStyle": false,
"markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}" "markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}"
}, },
@ -2996,172 +2996,6 @@
"id": "bb27723a-989c-2327-5808-b56d490b93ab", "id": "bb27723a-989c-2327-5808-b56d490b93ab",
"typeFullFqn": "system.cards.entities_table" "typeFullFqn": "system.cards.entities_table"
}, },
"e3a9539d-2ccb-96e0-2afa-36f93e40233d": {
"type": "latest",
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1684327643501,
"endTimeMs": 1684414043501
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
"limit": 200
}
},
"showTitle": true,
"backgroundColor": "rgb(255, 255, 255)",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "4px",
"settings": {
"entitiesTitle": "Devices",
"enableSearch": true,
"enableSelectColumnDisplay": true,
"enableStickyHeader": true,
"enableStickyAction": true,
"reserveSpaceForHiddenAction": "true",
"displayEntityName": true,
"entityNameColumnTitle": "Device Name",
"displayEntityLabel": false,
"displayEntityType": false,
"displayPagination": true,
"defaultPageSize": 10,
"defaultSortOrder": "entityName",
"useRowStyleFunction": false
},
"title": "Devices",
"dropShadow": true,
"enableFullscreen": true,
"titleStyle": {
"fontSize": "16px",
"fontWeight": 400,
"padding": "5px 10px 5px 10px"
},
"useDashboardTimewindow": false,
"showLegend": false,
"datasources": [
{
"type": "entity",
"name": null,
"entityAliasId": "a75d9031-ba51-8da4-81be-de65061b72f4",
"filterId": "3931abd5-2205-9386-6ea9-8e8a8131bb9d",
"dataKeys": [
{
"name": "type",
"type": "entityField",
"label": "Device Type",
"color": "#2196f3",
"settings": {},
"_hash": 0.3129929097366162,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
},
{
"name": "active",
"type": "attribute",
"label": "Status",
"color": "#4caf50",
"settings": {
"columnWidth": "0px",
"useCellStyleFunction": false,
"useCellContentFunction": true,
"cellContentFunction": "let cssClass;\r\nswitch (value) {\r\n case \"Active\":\r\n cssClass = \"status status-active\";\r\n break;\r\n case \"Inactive\":\r\n default:\r\n cssClass = \"status status-inactive\";\r\n break;\r\n }\r\n \r\n return `<span class='${cssClass}'>${value}</span>`;",
"defaultColumnVisibility": "visible",
"columnSelectionToDisplay": "enabled"
},
"_hash": 0.5969880627410065,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": true,
"postFuncBody": "return value == 'true' ? \"Active\": \"Inactive\";"
},
{
"name": "connectorName",
"type": "attribute",
"label": "Connector Name",
"color": "#f44336",
"settings": {},
"_hash": 0.012483045440007778,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
},
{
"name": "connectorType",
"type": "attribute",
"label": "Connector Type",
"color": "#ffc107",
"settings": {},
"_hash": 0.6004192233378134,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
],
"alarmFilterConfig": {
"statusList": [
"ACTIVE"
]
}
}
],
"displayTimewindow": true,
"showTitleIcon": false,
"titleTooltip": "",
"widgetStyle": {},
"widgetCss": ".status {\r\n border-radius: 20px;\r\n font-weight: 500;\r\n padding: 5px 15px;\r\n }\r\n\r\n .status-active {\r\n color: green;\r\n background: rgba(0, 128, 0, 0.1);\r\n }\r\n\r\n .status-inactive {\r\n color: red;\r\n background: rgba(255, 0, 0, 0.1);\r\n }\r\n",
"pageSize": 1024,
"noDataDisplayMessage": "",
"enableDataExport": false,
"actions": {
"actionCellButton": [
{
"name": "Show Device Info",
"icon": "info",
"useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;",
"type": "custom",
"customFunction": "const url = `${window.location.origin + widgetContext.utils.getEntityDetailsPageURL(entityId.id, entityId.entityType)}`;\nwindow.open(url, '_blank');",
"openInSeparateDialog": false,
"openInPopover": false,
"id": "94de7690-f91d-b032-6771-85af99abd749"
}
]
}
},
"row": 0,
"col": 0,
"id": "e3a9539d-2ccb-96e0-2afa-36f93e40233d",
"typeFullFqn": "system.cards.entities_table"
},
"cf2eba6b-44f6-9cc2-6089-35c735f54898": { "cf2eba6b-44f6-9cc2-6089-35c735f54898": {
"type": "latest", "type": "latest",
"sizeX": 7.5, "sizeX": 7.5,
@ -5632,33 +5466,6 @@
} }
} }
}, },
"gateway_devices_5": {
"name": "gateway_devices_opcua_async",
"root": false,
"layouts": {
"main": {
"widgets": {
"e3a9539d-2ccb-96e0-2afa-36f93e40233d": {
"sizeX": 24,
"sizeY": 11,
"row": 0,
"col": 0
}
},
"gridSettings": {
"backgroundColor": "#eeeeee",
"columns": 24,
"margin": 0,
"outerMargin": true,
"backgroundSizeMode": "100%",
"autoFillHeight": false,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
}
}
}
},
"gateway_devices_6": { "gateway_devices_6": {
"name": "gateway_devices_ble", "name": "gateway_devices_ble",
"root": false, "root": false,

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

@ -118,8 +118,8 @@ public class TenantProfileController extends BaseController {
"Let's review the example of tenant profile data below: " + "Let's review the example of tenant profile data below: " +
"\n\n" + MARKDOWN_CODE_BLOCK_START + "\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"name\": \"Default\",\n" + " \"name\": \"Your name\",\n" +
" \"description\": \"Default tenant profile\",\n" + " \"description\": \"Your description\",\n" +
" \"isolatedTbRuleEngine\": false,\n" + " \"isolatedTbRuleEngine\": false,\n" +
" \"profileData\": {\n" + " \"profileData\": {\n" +
" \"configuration\": {\n" + " \"configuration\": {\n" +
@ -162,7 +162,7 @@ public class TenantProfileController extends BaseController {
" \"warnThreshold\": 0\n" + " \"warnThreshold\": 0\n" +
" }\n" + " }\n" +
" },\n" + " },\n" +
" \"default\": true\n" + " \"default\": false\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END +
"Remove 'id', from the request body example (below) to create new Tenant Profile entity." + "Remove 'id', from the request body example (below) to create new Tenant Profile entity." +

65
application/src/main/java/org/thingsboard/server/service/edge/instructions/BaseEdgeInstallUpgradeInstructionsService.java

@ -0,0 +1,65 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.instructions;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
@RequiredArgsConstructor
public abstract class BaseEdgeInstallUpgradeInstructionsService {
private static final String EDGE_DIR = "edge";
private static final String INSTRUCTIONS_DIR = "instructions";
private final InstallScripts installScripts;
@Value("${app.version:unknown}")
@Setter
protected String appVersion;
protected String readFile(Path file) {
try {
return Files.readString(file);
} catch (IOException e) {
log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e);
}
}
protected String getTagVersion(String version) {
return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version;
}
protected Path resolveFile(String subDir, String... subDirs) {
return getEdgeInstructionsDir().resolve(Paths.get(subDir, subDirs));
}
protected Path getEdgeInstructionsDir() {
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, getBaseDirName());
}
protected abstract String getBaseDirName();
}

71
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java

@ -15,8 +15,7 @@
*/ */
package org.thingsboard.server.service.edge.instructions; package org.thingsboard.server.service.edge.instructions;
import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletRequest;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -27,47 +26,32 @@ import org.thingsboard.server.dao.util.DeviceConnectivityUtil;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.InstallScripts;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true")
@TbCoreComponent @TbCoreComponent
public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstructionsService { public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgradeInstructionsService implements EdgeInstallInstructionsService {
private static final String EDGE_DIR = "edge";
private static final String INSTRUCTIONS_DIR = "instructions";
private static final String INSTALL_DIR = "install"; private static final String INSTALL_DIR = "install";
private final InstallScripts installScripts;
@Value("${edges.rpc.port}") @Value("${edges.rpc.port}")
private int rpcPort; private int rpcPort;
@Value("${edges.rpc.ssl.enabled}") @Value("${edges.rpc.ssl.enabled}")
private boolean sslEnabled; private boolean sslEnabled;
@Value("${app.version:unknown}") public DefaultEdgeInstallInstructionsService(InstallScripts installScripts) {
@Setter super(installScripts);
private String appVersion; }
@Override @Override
public EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request) { public EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request) {
switch (installationMethod.toLowerCase()) { return switch (installationMethod.toLowerCase()) {
case "docker": case "docker" -> getDockerInstallInstructions(edge, request);
return getDockerInstallInstructions(edge, request); case "ubuntu", "centos" -> getLinuxInstallInstructions(edge, request, installationMethod.toLowerCase());
case "ubuntu": default ->
return getUbuntuInstallInstructions(edge, request); throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod);
case "centos": };
return getCentosInstallInstructions(edge, request);
default:
throw new IllegalArgumentException("Unsupported installation method for Edge: " + installationMethod);
}
} }
private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) { private EdgeInstructions getDockerInstallInstructions(Edge edge, HttpServletRequest request) {
@ -88,25 +72,16 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc
return new EdgeInstructions(dockerInstallInstructions); return new EdgeInstructions(dockerInstallInstructions);
} }
private EdgeInstructions getUbuntuInstallInstructions(Edge edge, HttpServletRequest request) { private EdgeInstructions getLinuxInstallInstructions(Edge edge, HttpServletRequest request, String os) {
String ubuntuInstallInstructions = readFile(resolveFile("ubuntu", "instructions.md")); String ubuntuInstallInstructions = readFile(resolveFile(os, "instructions.md"));
ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge); ubuntuInstallInstructions = replacePlaceholders(ubuntuInstallInstructions, edge);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName()); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${BASE_URL}", request.getServerName());
String edgeVersion = appVersion.replace("-SNAPSHOT", ""); String edgeVersion = appVersion.replace("-SNAPSHOT", "");
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion); ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
ubuntuInstallInstructions = ubuntuInstallInstructions.replace("${TB_EDGE_TAG}", getTagVersion(edgeVersion));
return new EdgeInstructions(ubuntuInstallInstructions); return new EdgeInstructions(ubuntuInstallInstructions);
} }
private EdgeInstructions getCentosInstallInstructions(Edge edge, HttpServletRequest request) {
String centosInstallInstructions = readFile(resolveFile("centos", "instructions.md"));
centosInstallInstructions = replacePlaceholders(centosInstallInstructions, edge);
centosInstallInstructions = centosInstallInstructions.replace("${BASE_URL}", request.getServerName());
String edgeVersion = appVersion.replace("-SNAPSHOT", "");
centosInstallInstructions = centosInstallInstructions.replace("${TB_EDGE_VERSION}", edgeVersion);
return new EdgeInstructions(centosInstallInstructions);
}
private String replacePlaceholders(String instructions, Edge edge) { private String replacePlaceholders(String instructions, Edge edge) {
instructions = instructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey()); instructions = instructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey());
instructions = instructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret()); instructions = instructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret());
@ -115,20 +90,8 @@ public class DefaultEdgeInstallInstructionsService implements EdgeInstallInstruc
return instructions; return instructions;
} }
private String readFile(Path file) { @Override
try { protected String getBaseDirName() {
return Files.readString(file); return INSTALL_DIR;
} catch (IOException e) {
log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e);
}
}
private Path resolveFile(String subDir, String... subDirs) {
return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs));
}
private Path getEdgeInstallInstructionsDir() {
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, INSTALL_DIR);
} }
} }

55
application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.edge.instructions; package org.thingsboard.server.service.edge.instructions;
import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -32,47 +31,37 @@ import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true")
@TbCoreComponent @TbCoreComponent
public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstructionsService { public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgradeInstructionsService implements EdgeUpgradeInstructionsService {
private static final Map<String, EdgeUpgradeInfo> upgradeVersionHashMap = new HashMap<>(); private static final Map<String, EdgeUpgradeInfo> upgradeVersionHashMap = new HashMap<>();
private static final String EDGE_DIR = "edge";
private static final String INSTRUCTIONS_DIR = "instructions";
private static final String UPGRADE_DIR = "upgrade"; private static final String UPGRADE_DIR = "upgrade";
private final InstallScripts installScripts;
private final AttributesService attributesService; private final AttributesService attributesService;
@Value("${app.version:unknown}") public DefaultEdgeUpgradeInstructionsService(AttributesService attributesService, InstallScripts installScripts) {
@Setter super(installScripts);
private String appVersion; this.attributesService = attributesService;
}
@Override @Override
public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) { public EdgeInstructions getUpgradeInstructions(String edgeVersion, String upgradeMethod) {
String tbVersion = appVersion.replace("-SNAPSHOT", ""); String tbVersion = appVersion.replace("-SNAPSHOT", "");
String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion); String currentEdgeVersion = convertEdgeVersionToDocsFormat(edgeVersion);
switch (upgradeMethod.toLowerCase()) { return switch (upgradeMethod.toLowerCase()) {
case "docker": case "docker" -> getDockerUpgradeInstructions(tbVersion, currentEdgeVersion);
return getDockerUpgradeInstructions(tbVersion, currentEdgeVersion); case "ubuntu", "centos" ->
case "ubuntu": getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase());
case "centos": default -> throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod);
return getLinuxUpgradeInstructions(tbVersion, currentEdgeVersion, upgradeMethod.toLowerCase()); };
default:
throw new IllegalArgumentException("Unsupported upgrade method for Edge: " + upgradeMethod);
}
} }
@Override @Override
@ -167,28 +156,12 @@ public class DefaultEdgeUpgradeInstructionsService implements EdgeUpgradeInstruc
return new EdgeInstructions(result.toString()); return new EdgeInstructions(result.toString());
} }
private String getTagVersion(String version) {
return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version;
}
private String convertEdgeVersionToDocsFormat(String edgeVersion) { private String convertEdgeVersionToDocsFormat(String edgeVersion) {
return edgeVersion.replace("_", ".").substring(2); return edgeVersion.replace("_", ".").substring(2);
} }
private String readFile(Path file) { @Override
try { protected String getBaseDirName() {
return Files.readString(file); return UPGRADE_DIR;
} catch (IOException e) {
log.warn("Failed to read file: {}", file, e);
throw new RuntimeException(e);
}
}
private Path resolveFile(String subDir, String... subDirs) {
return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs));
}
private Path getEdgeInstallInstructionsDir() {
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, UPGRADE_DIR);
} }
} }

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

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.service.subscription; package org.thingsboard.server.service.subscription;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
@ -24,9 +26,9 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
@ -51,8 +53,6 @@ import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscript
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -143,7 +143,28 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
* Even if we cache locally the list of active subscriptions by entity id, it is still time-consuming operation to get them from cache * Even if we cache locally the list of active subscriptions by entity id, it is still time-consuming operation to get them from cache
* Since number of subscriptions is usually much less than number of devices that are pushing data. * Since number of subscriptions is usually much less than number of devices that are pushing data.
*/ */
subscriptionsByEntityId.values().forEach(sub -> pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED))); Set<UUID> staleSubs = new HashSet<>();
subscriptionsByEntityId.forEach((id, sub) -> {
try {
pushSubEventToManagerService(sub.getTenantId(), sub.getEntityId(), sub.toEvent(ComponentLifecycleEvent.UPDATED));
} catch (TenantNotFoundException e) {
staleSubs.add(id);
log.warn("Cleaning up stale subscription {} for tenant {} due to TenantNotFoundException", id, sub.getTenantId());
} catch (Exception e) {
log.error("Failed to push subscription {} to manager service", sub, e);
}
});
if (!staleSubs.isEmpty()) {
subsLock.lock();
try {
staleSubs.forEach(entityId -> {
subscriptionsByEntityId.remove(entityId);
entityUpdates.remove(entityId);
});
} finally {
subsLock.unlock();
}
}
} }
} }

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

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

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -51,10 +52,11 @@ public class TenantProfile extends BaseData<TenantProfileId> implements HasName
@NoXss @NoXss
@Schema(description = "Description of the tenant profile", example = "Any text") @Schema(description = "Description of the tenant profile", example = "Any text")
private String description; private String description;
@Schema(description = "Default Tenant profile to be used.", example = "true") @Schema(description = "Default Tenant profile to be used.", example = "false")
@JsonProperty("default")
private boolean isDefault; private boolean isDefault;
@Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + @Schema(description = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " +
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "false")
private boolean isolatedTbRuleEngine; private boolean isolatedTbRuleEngine;
@Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.") @Schema(description = "Complex JSON object that contains profile settings: queue configs, max devices, max assets, rate limits, etc.")
private transient TenantProfileData profileData; private transient TenantProfileData profileData;

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.tenant.profile; package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -23,6 +24,7 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfileType; import org.thingsboard.server.common.data.TenantProfileType;
@Schema
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@Builder @Builder
@ -41,34 +43,61 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxOtaPackagesInBytes; private long maxOtaPackagesInBytes;
private long maxResourceSize; private long maxResourceSize;
@Schema(example = "1000:1,20000:60")
private String transportTenantMsgRateLimit; private String transportTenantMsgRateLimit;
@Schema(example = "1000:1,20000:60")
private String transportTenantTelemetryMsgRateLimit; private String transportTenantTelemetryMsgRateLimit;
@Schema(example = "1000:1,20000:60")
private String transportTenantTelemetryDataPointsRateLimit; private String transportTenantTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String transportDeviceMsgRateLimit; private String transportDeviceMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportDeviceTelemetryMsgRateLimit; private String transportDeviceTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportDeviceTelemetryDataPointsRateLimit; private String transportDeviceTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayMsgRateLimit; private String transportGatewayMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayTelemetryMsgRateLimit; private String transportGatewayTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayTelemetryDataPointsRateLimit; private String transportGatewayTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayDeviceMsgRateLimit; private String transportGatewayDeviceMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayDeviceTelemetryMsgRateLimit; private String transportGatewayDeviceTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
private String transportGatewayDeviceTelemetryDataPointsRateLimit; private String transportGatewayDeviceTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
private String tenantEntityExportRateLimit; private String tenantEntityExportRateLimit;
@Schema(example = "20:1,600:60")
private String tenantEntityImportRateLimit; private String tenantEntityImportRateLimit;
@Schema(example = "20:1,600:60")
private String tenantNotificationRequestsRateLimit; private String tenantNotificationRequestsRateLimit;
@Schema(example = "20:1,600:60")
private String tenantNotificationRequestsPerRuleRateLimit; private String tenantNotificationRequestsPerRuleRateLimit;
@Schema(example = "10000000")
private long maxTransportMessages; private long maxTransportMessages;
@Schema(example = "10000000")
private long maxTransportDataPoints; private long maxTransportDataPoints;
@Schema(example = "4000000")
private long maxREExecutions; private long maxREExecutions;
@Schema(example = "5000000")
private long maxJSExecutions; private long maxJSExecutions;
@Schema(example = "5000000")
private long maxTbelExecutions; private long maxTbelExecutions;
@Schema(example = "0")
private long maxDPStorageDays; private long maxDPStorageDays;
@Schema(example = "50")
private int maxRuleNodeExecutionsPerMessage; private int maxRuleNodeExecutionsPerMessage;
@Schema(example = "0")
private long maxEmails; private long maxEmails;
@Schema(example = "true")
private Boolean smsEnabled; private Boolean smsEnabled;
@Schema(example = "0")
private long maxSms; private long maxSms;
@Schema(example = "1000")
private long maxCreatedAlarms; private long maxCreatedAlarms;
private String tenantServerRestLimitsConfiguration; private String tenantServerRestLimitsConfiguration;

3
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java

@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.TenantProfileType; import org.thingsboard.server.common.data.TenantProfileType;
@ -31,6 +33,7 @@ import java.io.Serializable;
property = "type") property = "type")
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")}) @JsonSubTypes.Type(value = DefaultTenantProfileConfiguration.class, name = "DEFAULT")})
@Schema(discriminatorProperty = "type", discriminatorMapping = {@DiscriminatorMapping(value = "DEFAULT", schema = DefaultTenantProfileConfiguration.class)})
public interface TenantProfileConfiguration extends Serializable { public interface TenantProfileConfiguration extends Serializable {
@JsonIgnore @JsonIgnore

2
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>common</artifactId> <artifactId>common</artifactId>

2
common/proto/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/script/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/script/remote-js-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>script</artifactId> <artifactId>script</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.script</groupId> <groupId>org.thingsboard.common.script</groupId>

2
common/script/script-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>script</artifactId> <artifactId>script</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.script</groupId> <groupId>org.thingsboard.common.script</groupId>

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.transport</groupId> <groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.transport</groupId> <groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.transport</groupId> <groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.transport</groupId> <groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common.transport</groupId> <groupId>org.thingsboard.common.transport</groupId>

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/version-control/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>dao</artifactId> <artifactId>dao</artifactId>

7
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.rule;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
@ -51,9 +52,9 @@ public interface RuleNodeRepository extends JpaRepository<RuleNodeEntity, UUID>
Pageable pageable); Pageable pageable);
@Query("SELECT r.id FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version") @Query("SELECT r.id FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version")
Page<UUID> findAllRuleNodeIdsByTypeAndVersionLessThan(@Param("ruleType") String ruleType, Slice<UUID> findAllRuleNodeIdsByTypeAndVersionLessThan(@Param("ruleType") String ruleType,
@Param("version") int version, @Param("version") int version,
Pageable pageable); Pageable pageable);
List<RuleNodeEntity> findRuleNodesByRuleChainIdAndExternalIdIn(UUID ruleChainId, List<UUID> externalIds); List<RuleNodeEntity> findRuleNodesByRuleChainIdAndExternalIdIn(UUID ruleChainId, List<UUID> externalIds);

7
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -239,10 +240,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
long minPartition = toPartitionTs(query.getStartTs()); long minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs()); long maxPartition = toPartitionTs(query.getEndTs());
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>(); final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
final ListenableFuture<List<Long>> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); final ListenableFuture<List<Long>> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition);
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() { Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
@Override @Override
@ -416,7 +415,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
readResultsProcessingExecutor); readResultsProcessingExecutor);
} }
private ListenableFuture<List<Long>> getPartitionsFuture(TenantId tenantId, ReadTsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { private ListenableFuture<List<Long>> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) {
if (isFixedPartitioning()) { //no need to fetch partitions from DB if (isFixedPartitioning()) { //no need to fetch partitions from DB
return Futures.immediateFuture(FIXED_PARTITION); return Futures.immediateFuture(FIXED_PARTITION);
} }

52
dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java

@ -45,6 +45,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -73,6 +74,9 @@ class DeviceProfileDataValidatorTest {
" \"clientOnlyObserveAfterConnect\": 1\n" + " \"clientOnlyObserveAfterConnect\": 1\n" +
" }"; " }";
private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!";
private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!";
private static final String msgErrorNotNull = " Server ShortServerId must not be null!";
private static final String host = "localhost"; private static final String host = "localhost";
private static final String hostBs = "localhost"; private static final String hostBs = "localhost";
@ -116,19 +120,50 @@ class DeviceProfileDataValidatorTest {
validator.validateDataImpl(tenantId, deviceProfile); validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName()); verify(validator).validateString("Device profile name", deviceProfile.getName());
} }
@Test @Test
void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() { void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() {
Integer shortServerId = 123; Integer shortServerId = 123;
Integer shortServerIdBs = 0; Integer shortServerIdBs = 0;
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs));
DeviceProfile deviceProfile = getDeviceProfile(transportConfiguration);
validator.validateDataImpl(tenantId, deviceProfile); validator.validateDataImpl(tenantId, deviceProfile);
verify(validator).validateString("Device profile name", deviceProfile.getName()); verify(validator).validateString("Device profile name", deviceProfile.getName());
} }
private DeviceProfile getDeviceProfile(Lwm2mDeviceProfileTransportConfiguration transportConfiguration) { @Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() {
verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_More_65535_Error() {
verifyValidationError(123, 65536, msgErrorBsRange);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_Less_0_Error() {
verifyValidationError(123, -1, msgErrorBsRange);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_null_Error_BootstrapShortServerId_Ok() {
verifyValidationError(null, 1, "LwM2M" + msgErrorNotNull);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() {
verifyValidationError(65535, 111, msgErrorLwm2mRange);
}
@Test
void testValidateDeviceProfile_Lwm2mShortServerId_Less_1_Error_BootstrapShortServerId_Ok() {
verifyValidationError(0, 111, msgErrorLwm2mRange);
}
private DeviceProfile getDeviceProfile(Integer shortServerId, Integer shortServerIdBs) {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration =
getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(shortServerId, shortServerIdBs));
DeviceProfile deviceProfile = new DeviceProfile(); DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setName("default"); deviceProfile.setName("default");
deviceProfile.setType(DeviceProfileType.DEFAULT); deviceProfile.setType(DeviceProfileType.DEFAULT);
@ -151,7 +186,7 @@ class DeviceProfileDataValidatorTest {
return transportConfiguration; return transportConfiguration;
} }
private List<LwM2MBootstrapServerCredential> getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs){ private List<LwM2MBootstrapServerCredential> getBootstrapServerCredentialsNoSec(Integer shortServerId, Integer shortServerIdBs) {
List<LwM2MBootstrapServerCredential> bootstrap = new ArrayList<>(); List<LwM2MBootstrapServerCredential> bootstrap = new ArrayList<>();
bootstrap.add(getBootstrapServerCredentialNoSec(false, shortServerId, shortServerIdBs)); bootstrap.add(getBootstrapServerCredentialNoSec(false, shortServerId, shortServerIdBs));
bootstrap.add(getBootstrapServerCredentialNoSec(true, shortServerId, shortServerIdBs)); bootstrap.add(getBootstrapServerCredentialNoSec(true, shortServerId, shortServerIdBs));
@ -168,4 +203,11 @@ class DeviceProfileDataValidatorTest {
return bootstrapServerCredential; return bootstrapServerCredential;
} }
private void verifyValidationError(Integer shortServerId, Integer shortServerIdBs, String msgError) {
DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs);
assertThatThrownBy(() -> validator.validateDataImpl(tenantId, deviceProfile))
.hasMessageContaining(msgError);
}
} }

16
dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java

@ -22,7 +22,6 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.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.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -127,20 +126,15 @@ public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest {
@Test @Test
public void testFindRuleNodeIdsByTypeAndVersionLessThan() { public void testFindRuleNodeIdsByTypeAndVersionLessThan() {
// test - search text ignored
PageData<RuleNodeId> ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME)); PageData<RuleNodeId> ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME));
assertEquals(20, ruleNodeIds.getTotalElements()); assertEquals(0, ruleNodeIds.getTotalElements()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(2, ruleNodeIds.getTotalPages()); assertEquals(0, ruleNodeIds.getTotalPages()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(10, ruleNodeIds.getData().size()); assertEquals(10, ruleNodeIds.getData().size());
ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0)); ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0));
assertEquals(20, ruleNodeIds.getTotalElements()); assertEquals(0, ruleNodeIds.getTotalElements()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(2, ruleNodeIds.getTotalPages()); assertEquals(0, ruleNodeIds.getTotalPages()); // due to DaoUtil.pageToPageData impl for Slice
assertEquals(10, ruleNodeIds.getData().size());
// test - search text ignored
ruleNodeIds = ruleNodeDao.findAllRuleNodeIdsByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, StringUtils.randomAlphabetic(5)));
assertEquals(20, ruleNodeIds.getTotalElements());
assertEquals(2, ruleNodeIds.getTotalPages());
assertEquals(10, ruleNodeIds.getData().size()); assertEquals(10, ruleNodeIds.getData().size());
} }

2
monitoring/pom.xml

@ -21,7 +21,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>

91
monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java

@ -30,10 +30,12 @@ import org.eclipse.leshan.client.californium.LeshanClientBuilder;
import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.client.object.Server; import org.eclipse.leshan.client.object.Server;
import org.eclipse.leshan.client.observer.LwM2mClientObserver;
import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.BaseInstanceEnabler;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer; import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.client.servers.ServerIdentity; import org.eclipse.leshan.client.servers.ServerIdentity;
import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.californium.EndpointFactory; import org.eclipse.leshan.core.californium.EndpointFactory;
import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.LwM2mModel;
@ -42,6 +44,10 @@ import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.StaticModel; import org.eclipse.leshan.core.model.StaticModel;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder;
import org.eclipse.leshan.core.request.BootstrapRequest;
import org.eclipse.leshan.core.request.DeregisterRequest;
import org.eclipse.leshan.core.request.RegisterRequest;
import org.eclipse.leshan.core.request.UpdateRequest;
import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.core.response.ReadResponse;
import org.thingsboard.monitoring.util.ResourceUtils; import org.thingsboard.monitoring.util.ResourceUtils;
@ -96,7 +102,7 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
LwM2mModel model = new StaticModel(models); LwM2mModel model = new StaticModel(models);
ObjectsInitializer initializer = new ObjectsInitializer(model); ObjectsInitializer initializer = new ObjectsInitializer(model);
initializer.setInstancesForObject(SECURITY, security); initializer.setInstancesForObject(SECURITY, security);
initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(60))); initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(5)));
initializer.setInstancesForObject(DEVICE, this); initializer.setInstancesForObject(DEVICE, this);
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();
@ -139,6 +145,89 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable {
builder.setEncoder(new DefaultLwM2mEncoder(false)); builder.setEncoder(new DefaultLwM2mEncoder(false));
leshanClient = builder.build(); leshanClient = builder.build();
LwM2mClientObserver observer = new LwM2mClientObserver() {
@Override
public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) {}
@Override
public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) {}
@Override
public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request,
ResponseCode responseCode, String errorMessage, Exception cause) {}
@Override
public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) {}
@Override
public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) {
log.debug("onRegistrationStarted [{}]", request.getEndpointName());
}
@Override
public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) {
log.debug("onRegistrationSuccess [{}] [{}]", request.getEndpointName(), registrationID);
}
@Override
public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onRegistrationFailure [{}] [{}] [{}]", request.getEndpointName(), responseCode, errorMessage);
}
@Override
public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) {
log.debug("onRegistrationTimeout [{}]", request.getEndpointName());
}
@Override
public void onUpdateStarted(ServerIdentity server, UpdateRequest request) {
log.debug("onUpdateStarted [{}]", request.getRegistrationId());
}
@Override
public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) {
log.debug("onUpdateSuccess [{}]", request.getRegistrationId());
}
@Override
public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onUpdateFailure [{}]", request.getRegistrationId());
}
@Override
public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) {
log.debug("onUpdateTimeout [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationStarted [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationStarted [{}]", request.getRegistrationId());
}
@Override
public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) {
log.debug("onDeregistrationFailure [{}] [{}] [{}]", request.getRegistrationId(), responseCode, errorMessage);
}
@Override
public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) {
log.debug("onDeregistrationTimeout [{}]", request.getRegistrationId());
}
@Override
public void onUnexpectedError(Throwable unexpectedError) {
log.debug("onUnexpectedError [{}]", unexpectedError.toString());
}
};
leshanClient.addObserver(observer);
setLeshanClient(leshanClient); setLeshanClient(leshanClient);
leshanClient.start(); leshanClient.start();

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/js-executor/package.json

@ -1,7 +1,7 @@
{ {
"name": "thingsboard-js-executor", "name": "thingsboard-js-executor",
"private": true, "private": true,
"version": "3.7.0", "version": "3.7.1",
"description": "ThingsBoard JavaScript Executor Microservice", "description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts", "main": "server.ts",
"bin": "server.js", "bin": "server.js",

2
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/monitoring/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>

2
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa.transport</groupId> <groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa.transport</groupId> <groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa.transport</groupId> <groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa.transport</groupId> <groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent> <parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
</parent> </parent>
<groupId>org.thingsboard.msa.transport</groupId> <groupId>org.thingsboard.msa.transport</groupId>

2
msa/vc-executor-docker/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/vc-executor/pom.xml

@ -21,7 +21,7 @@
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

2
msa/web-ui/package.json

@ -1,7 +1,7 @@
{ {
"name": "thingsboard-web-ui", "name": "thingsboard-web-ui",
"private": true, "private": true,
"version": "3.7.0", "version": "3.7.1",
"description": "ThingsBoard Web UI Microservice", "description": "ThingsBoard Web UI Microservice",
"main": "server.ts", "main": "server.ts",
"bin": "server.js", "bin": "server.js",

2
msa/web-ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>msa</artifactId> <artifactId>msa</artifactId>
</parent> </parent>
<groupId>org.thingsboard.msa</groupId> <groupId>org.thingsboard.msa</groupId>

4
netty-mqtt/pom.xml

@ -19,11 +19,11 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>netty-mqtt</artifactId> <artifactId>netty-mqtt</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Netty MQTT Client</name> <name>Netty MQTT Client</name>

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard</name> <name>Thingsboard</name>

2
rest-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>rest-client</artifactId> <artifactId>rest-client</artifactId>

2
rule-engine/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>rule-engine</artifactId> <artifactId>rule-engine</artifactId>

2
rule-engine/rule-engine-api/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>rule-engine</artifactId> <artifactId>rule-engine</artifactId>
</parent> </parent>
<groupId>org.thingsboard.rule-engine</groupId> <groupId>org.thingsboard.rule-engine</groupId>

2
rule-engine/rule-engine-components/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>rule-engine</artifactId> <artifactId>rule-engine</artifactId>
</parent> </parent>
<groupId>org.thingsboard.rule-engine</groupId> <groupId>org.thingsboard.rule-engine</groupId>

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

@ -28,12 +28,14 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "rest api call", name = "rest api call",
configClazz = TbRestApiCallNodeConfiguration.class, configClazz = TbRestApiCallNodeConfiguration.class,
version = 1, version = 2,
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." +
@ -58,9 +60,6 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
super.init(ctx); super.init(ctx);
TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
httpClient = new TbHttpClient(config, ctx.getSharedEventLoop()); httpClient = new TbHttpClient(config, ctx.getSharedEventLoop());
if (config.isUseRedisQueueForMsgPersistence()) {
log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId());
}
} }
@Override @Override
@ -88,6 +87,11 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
((ObjectNode) oldConfiguration).put(PARSE_TO_PLAIN_TEXT, oldConfiguration.get(TRIM_DOUBLE_QUOTES).booleanValue()); ((ObjectNode) oldConfiguration).put(PARSE_TO_PLAIN_TEXT, oldConfiguration.get(TRIM_DOUBLE_QUOTES).booleanValue());
((ObjectNode) oldConfiguration).remove(TRIM_DOUBLE_QUOTES); ((ObjectNode) oldConfiguration).remove(TRIM_DOUBLE_QUOTES);
} }
case 1:
if (oldConfiguration.has("useRedisQueueForMsgPersistence")) {
hasChanges = true;
((ObjectNode) oldConfiguration).remove(List.of("useRedisQueueForMsgPersistence", "trimQueue", "maxQueueSize"));
}
break; break;
default: default:
break; break;

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

@ -36,7 +36,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
private boolean useSimpleClientHttpFactory; private boolean useSimpleClientHttpFactory;
private int readTimeoutMs; private int readTimeoutMs;
private int maxParallelRequestsCount; private int maxParallelRequestsCount;
private boolean useRedisQueueForMsgPersistence;
private boolean parseToPlainText; private boolean parseToPlainText;
private boolean enableProxy; private boolean enableProxy;
private boolean useSystemProxyProperties; private boolean useSystemProxyProperties;
@ -57,7 +56,6 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setUseSimpleClientHttpFactory(false); configuration.setUseSimpleClientHttpFactory(false);
configuration.setReadTimeoutMs(0); configuration.setReadTimeoutMs(0);
configuration.setMaxParallelRequestsCount(0); configuration.setMaxParallelRequestsCount(0);
configuration.setUseRedisQueueForMsgPersistence(false);
configuration.setParseToPlainText(false); configuration.setParseToPlainText(false);
configuration.setEnableProxy(false); configuration.setEnableProxy(false);
configuration.setCredentials(new AnonymousCredentials()); configuration.setCredentials(new AnonymousCredentials());

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

@ -16,7 +16,6 @@
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;
@ -27,13 +26,16 @@ import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler; import org.apache.http.protocol.HttpRequestHandler;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -41,7 +43,6 @@ 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;
@ -50,6 +51,7 @@ import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNotSame;
@ -57,8 +59,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class TbRestApiCallNodeTest { public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
@Spy
private TbRestApiCallNode restNode; private TbRestApiCallNode restNode;
@Mock @Mock
@ -214,20 +217,45 @@ public class TbRestApiCallNodeTest {
assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue());
} }
@Test private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { return Stream.of(
var defaultConfig = new TbRestApiCallNodeConfiguration().defaultConfiguration(); // config for version 2 with upgrade from version 0
var node = new TbRestApiCallNode(); Arguments.of(0,
String oldConfig = "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," + "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\":\"POST\"," +
"\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," + "\"useSimpleClientHttpFactory\":false,\"ignoreRequestBody\":false,\"enableProxy\":false," +
"\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," + "\"useSystemProxyProperties\":false,\"proxyScheme\":null,\"proxyHost\":null,\"proxyPort\":0," +
"\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," + "\"proxyUser\":null,\"proxyPassword\":null,\"readTimeoutMs\":0,\"maxParallelRequestsCount\":0," +
"\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," + "\"headers\":{\"Content-Type\":\"application/json\"},\"useRedisQueueForMsgPersistence\":false," +
"\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":true}"; "\"trimQueue\":null,\"maxQueueSize\":null,\"credentials\":{\"type\":\"anonymous\"},\"trimDoubleQuotes\":false}",
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); true,
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson); "{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
Assertions.assertTrue(upgrade.getFirst()); "\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": false,\"ignoreRequestBody\": false," +
Assertions.assertTrue(JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()).isParseToPlainText()); "\"enableProxy\": false,\"useSystemProxyProperties\": false,\"proxyScheme\": null,\"proxyHost\": null," +
"\"proxyPort\": 0,\"proxyUser\": null,\"proxyPassword\": null,\"readTimeoutMs\": 0," +
"\"maxParallelRequestsCount\": 0,\"headers\": {\"Content-Type\": \"application/json\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}"),
// config for version 2 with upgrade from version 1
Arguments.of(1,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": 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\"}}",
true,
"{\"restEndpointUrlPattern\":\"http://localhost/api\",\"requestMethod\": \"POST\"," +
"\"useSimpleClientHttpFactory\": false,\"parseToPlainText\": 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\"}," +
"\"credentials\": {\"type\": \"anonymous\"}}")
);
}
@Override
protected TbNode getTestNode() {
return restNode;
} }
} }

87
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java

@ -19,8 +19,12 @@ import com.google.common.util.concurrent.SettableFuture;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.common.util.ListeningExecutor;
@ -29,7 +33,10 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
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.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -37,24 +44,30 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class TbSendRPCReplyNodeTest { public class TbSendRPCReplyNodeTest {
private static final String DUMMY_SERVICE_ID = "testServiceId"; private static final String DUMMY_SERVICE_ID = "testServiceId";
private static final int DUMMY_REQUEST_ID = 0; private static final int DUMMY_REQUEST_ID = 0;
private static final UUID DUMMY_SESSION_ID = UUID.randomUUID(); private static final UUID DUMMY_SESSION_ID = UUID.fromString("4f1d94aa-f6ee-4078-8499-b8e68443f8ad");
private static final String DUMMY_DATA = "{\"key\":\"value\"}"; private final String DUMMY_DATA = "{\"key\":\"value\"}";
TbSendRPCReplyNode node; private TbSendRPCReplyNode node;
private TbSendRpcReplyNodeConfiguration config;
private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); private final TenantId tenantId = TenantId.fromUUID(UUID.fromString("4e2e2336-3376-4238-ba0a-c669b412ca66"));
private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); private final DeviceId deviceId = new DeviceId(UUID.fromString("af64d1b9-8635-47e1-8738-6389df7fe57e"));
@Mock @Mock
private TbContext ctx; private TbContext ctx;
@ -71,14 +84,13 @@ public class TbSendRPCReplyNodeTest {
@BeforeEach @BeforeEach
public void setUp() throws TbNodeException { public void setUp() throws TbNodeException {
node = new TbSendRPCReplyNode(); node = new TbSendRPCReplyNode();
TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration();
node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
} }
@Test @Test
public void sendReplyToTransport() { public void sendReplyToTransport() {
Mockito.when(ctx.getRpcService()).thenReturn(rpcService); when(ctx.getRpcService()).thenReturn(rpcService);
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, getDefaultMetadata(), TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, getDefaultMetadata(),
TbMsgDataType.JSON, DUMMY_DATA, null, null); TbMsgDataType.JSON, DUMMY_DATA, null, null);
@ -91,10 +103,10 @@ public class TbSendRPCReplyNodeTest {
@Test @Test
public void sendReplyToEdgeQueue() { public void sendReplyToEdgeQueue() {
Mockito.when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getTenantId()).thenReturn(tenantId);
Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService); when(ctx.getEdgeEventService()).thenReturn(edgeEventService);
Mockito.when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create()); when(edgeEventService.saveAsync(any())).thenReturn(SettableFuture.create());
Mockito.when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor); when(ctx.getDbCallbackExecutor()).thenReturn(listeningExecutor);
TbMsgMetaData defaultMetadata = getDefaultMetadata(); TbMsgMetaData defaultMetadata = getDefaultMetadata();
defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString()); defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString());
@ -108,6 +120,55 @@ public class TbSendRPCReplyNodeTest {
verify(rpcService, never()).sendRpcReplyToDevice(DUMMY_SERVICE_ID, DUMMY_SESSION_ID, DUMMY_REQUEST_ID, DUMMY_DATA); verify(rpcService, never()).sendRpcReplyToDevice(DUMMY_SERVICE_ID, DUMMY_SESSION_ID, DUMMY_REQUEST_ID, DUMMY_DATA);
} }
@ParameterizedTest
@EnumSource(EntityType.class)
public void testOriginatorEntityTypes(EntityType entityType) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "0f386739-210f-4e23-8739-23f84a172adc");
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class)
.hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!"
: "Request id is not present in the metadata!");
}
@ParameterizedTest
@MethodSource
public void testForAvailabilityOfMetadataAndDataValues(TbMsgMetaData metaData, String errorMsg) {
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metaData, TbMsg.EMPTY_STRING);
node.onMsg(ctx, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage(errorMsg);
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getServiceIdMetaDataAttribute()).isEqualTo("serviceId");
assertThat(config.getSessionIdMetaDataAttribute()).isEqualTo("sessionId");
assertThat(config.getRequestIdMetaDataAttribute()).isEqualTo("requestId");
}
private static Stream<Arguments> testForAvailabilityOfMetadataAndDataValues() {
return Stream.of(
Arguments.of(TbMsgMetaData.EMPTY, "Request id is not present in the metadata!"),
Arguments.of(new TbMsgMetaData(Map.of(
"requestId", Integer.toString(DUMMY_REQUEST_ID))), "Service id is not present in the metadata!"),
Arguments.of(new TbMsgMetaData(Map.of(
"requestId", Integer.toString(DUMMY_REQUEST_ID),
"serviceId", DUMMY_SERVICE_ID)), "Session id is not present in the metadata!"),
Arguments.of(new TbMsgMetaData(Map.of(
"requestId", Integer.toString(DUMMY_REQUEST_ID),
"serviceId", DUMMY_SERVICE_ID, "sessionId",
DUMMY_SESSION_ID.toString())), "Request body is empty!")
);
}
private TbMsgMetaData getDefaultMetadata() { private TbMsgMetaData getDefaultMetadata() {
TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration(); TbSendRpcReplyNodeConfiguration config = new TbSendRpcReplyNodeConfiguration().defaultConfiguration();
TbMsgMetaData metadata = new TbMsgMetaData(); TbMsgMetaData metadata = new TbMsgMetaData();

421
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNodeTest.java

@ -0,0 +1,421 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.rpc;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
public class TbSendRPCRequestNodeTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("d3a47f8b-d863-4c1f-b6f0-2c946b43f21c"));
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b052ae59-b9b4-47e8-ac71-39e7124bbd66"));
private final String MSG_DATA = """
{
"method": "setGpio",
"params": {
"pin": "23",
"value": 1
},
"additionalInfo": "information"
}
""";
private TbSendRPCRequestNode node;
private TbSendRpcRequestNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private RuleEngineRpcService rpcServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = new TbSendRPCRequestNode();
config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration();
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, configuration);
}
@Test
public void verifyDefaultConfig() {
assertThat(config.getTimeoutInSeconds()).isEqualTo(60);
}
@ParameterizedTest
@MethodSource
public void givenOneway_whenOnMsg_thenVerifyRequest(String mdKeyValue, boolean expectedResult) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData msgMetadata = new TbMsgMetaData();
msgMetadata.putValue("oneway", mdKeyValue);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA);
node.onMsg(ctxMock, msg);
var ruleEngineDeviceRpcRequestCaptor = captureRequest();
assertThat(ruleEngineDeviceRpcRequestCaptor.getValue().isOneway()).isEqualTo(expectedResult);
}
private static Stream<Arguments> givenOneway_whenOnMsg_thenVerifyRequest() {
return Stream.of(
Arguments.of("true", true),
Arguments.of("false", false),
Arguments.of(null, false),
Arguments.of("", false)
);
}
@Test
public void givenMsgBody_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class);
then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class));
assertThat(requestCaptor.getValue())
.hasFieldOrPropertyWithValue("method", "setGpio")
.hasFieldOrPropertyWithValue("body", "{\"pin\":\"23\",\"value\":1}")
.hasFieldOrPropertyWithValue("deviceId", DEVICE_ID)
.hasFieldOrPropertyWithValue("tenantId", TENANT_ID)
.hasFieldOrPropertyWithValue("additionalInfo", "information");
}
@Test
public void givenRequestIdIsNotSet_whenOnMsg_thenVerifyRequest() {
Random randomMock = mock(Random.class);
given(randomMock.nextInt()).willReturn(123);
ReflectionTestUtils.setField(node, "random", randomMock);
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(123);
}
@Test
public void givenRequestId_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String data = """
{
"method": "setGpio",
"params": {
"pin": "23",
"value": 1
},
"requestId": 12345
}
""";
TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(12345);
}
@Test
public void givenRequestUUID_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String requestUUID = "b795a241-5a30-48fb-92d5-46b864d47130";
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("requestUUID", requestUUID);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestUUID()).isEqualTo(UUID.fromString(requestUUID));
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidRequestUUID_whenOnMsg_thenVerifyRequest(String requestUUID) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("requestUUID", requestUUID);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRequestUUID()).isNotNull();
}
@Test
public void givenOriginServiceId_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String originServiceId = "service-id-123";
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("originServiceId", originServiceId);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getOriginServiceId()).isEqualTo(originServiceId);
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidOriginServiceId_whenOnMsg_thenVerifyRequest(String originServiceId) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue("originServiceId", originServiceId);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getOriginServiceId()).isNull();
}
@Test
public void givenExpirationTime_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
String expirationTime = "2000000000000";
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getExpirationTime()).isEqualTo(Long.parseLong(expirationTime));
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidExpirationTime_whenOnMsg_thenVerifyRequest(String expirationTime) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getExpirationTime()).isGreaterThan(System.currentTimeMillis());
}
@Test
public void givenRetries_whenOnMsg_thenVerifyRequest() {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
Integer retries = 3;
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.RETRIES, String.valueOf(retries));
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRetries()).isEqualTo(retries);
}
@ParameterizedTest
@NullAndEmptySource
public void givenInvalidRetriesValue_whenOnMsg_thenVerifyRequest(String retries) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.RETRIES, retries);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().getRetries()).isNull();
}
@ParameterizedTest
@EnumSource(TbMsgType.class)
public void givenTbMsgType_whenOnMsg_thenVerifyRequest(TbMsgType msgType) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
if (msgType == TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE) {
assertThat(requestCaptor.getValue().isRestApiCall()).isTrue();
return;
}
assertThat(requestCaptor.getValue().isRestApiCall()).isFalse();
}
@ParameterizedTest
@MethodSource
public void givenPersistent_whenOnMsg_thenVerifyRequest(String isPersisted, boolean expectedPersistence) {
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
TbMsgMetaData metadata = new TbMsgMetaData();
metadata.putValue(DataConstants.PERSISTENT, isPersisted);
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA);
node.onMsg(ctxMock, msg);
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest();
assertThat(requestCaptor.getValue().isPersisted()).isEqualTo(expectedPersistence);
}
private static Stream<Arguments> givenPersistent_whenOnMsg_thenVerifyRequest() {
return Stream.of(
Arguments.of("true", true),
Arguments.of("false", false),
Arguments.of(null, false),
Arguments.of("", false)
);
}
private ArgumentCaptor<RuleEngineDeviceRpcRequest> captureRequest() {
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class);
then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class));
return requestCaptor;
}
@Test
public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() {
TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
// TODO: replace deprecated method newMsg()
given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg);
willAnswer(invocation -> {
Consumer<RuleEngineDeviceRpcResponse> consumer = invocation.getArgument(1);
RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class);
given(rpcResponseMock.getError()).willReturn(Optional.empty());
given(rpcResponseMock.getResponse()).willReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT));
consumer.accept(rpcResponseMock);
return null;
}).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class));
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
then(ctxMock).should().enqueueForTellNext(outMsg, TbNodeConnectionType.SUCCESS);
then(ctxMock).should().ack(msg);
}
@Test
public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() {
TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
given(ctxMock.getRpcService()).willReturn(rpcServiceMock);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
// TODO: replace deprecated method newMsg()
given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg);
willAnswer(invocation -> {
Consumer<RuleEngineDeviceRpcResponse> consumer = invocation.getArgument(1);
RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class);
given(rpcResponseMock.getError()).willReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION));
consumer.accept(rpcResponseMock);
return null;
}).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class));
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA);
node.onMsg(ctxMock, msg);
then(ctxMock).should().enqueueForTellFailure(outMsg, RpcError.NO_ACTIVE_CONNECTION.name());
then(ctxMock).should().ack(msg);
}
@ParameterizedTest
@EnumSource(EntityType.class)
public void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "ac21a1bb-eabf-4463-8313-24bea1f498d9");
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class)
.hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!"
: "Method is not present in the message!");
}
@ParameterizedTest
@ValueSource(strings = {"method", "params"})
public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) {
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}");
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class)
.hasMessage(key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!");
}
}

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>tools</artifactId> <artifactId>tools</artifactId>

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>

2
transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>

2
ui-ngx/package.json

@ -1,6 +1,6 @@
{ {
"name": "thingsboard", "name": "thingsboard",
"version": "3.7.0", "version": "3.7.1",
"scripts": { "scripts": {
"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",

2
ui-ngx/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>3.7.0-SNAPSHOT</version> <version>3.7.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

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

@ -28,7 +28,7 @@
<div class="tb-form-table-body no-gap"> <div class="tb-form-table-body no-gap">
<div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding" <div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding"
[class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL"> [class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL">
<div class="fixed-title-width" translate>gateway.device-info.name</div> <div class="fixed-title-width tb-required" translate>gateway.device-info.name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource"> <div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource"> <mat-select formControlName="deviceNameExpressionSource">
@ -61,7 +61,7 @@
</div> </div>
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding" <div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding"
*ngIf="deviceInfoType === DeviceInfoType.FULL"> *ngIf="deviceInfoType === DeviceInfoType.FULL">
<div class="fixed-title-width" translate>gateway.device-info.profile-name</div> <div class="fixed-title-width tb-required" translate>gateway.device-info.profile-name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource"> <div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceProfileExpressionSource"> <mat-select formControlName="deviceProfileExpressionSource">

65
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.html

@ -0,0 +1,65 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fields-label" translate>gateway.security</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type">
{{ SecurityTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container *ngIf="securityFormGroup.get('type').value === BrokerSecurityType.BASIC">
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.username</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.username-required') | translate"
*ngIf="securityFormGroup.get('username').hasError('required') && securityFormGroup.get('username').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.password</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.password-required') | translate"
*ngIf="securityFormGroup.get('password').hasError('required')
&& securityFormGroup.get('password').touched"
class="tb-error">
warning
</mat-icon>
<div [class.hide-toggle]="securityFormGroup.get('password').hasError('required')" class="tb-flex no-gap align-center fill-height" matSuffix>
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password>
</div>
</mat-form-field>
</div>
</div>
</ng-container>
</div>

29
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.scss

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

132
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.ts

@ -0,0 +1,132 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import { Subject } from 'rxjs';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { takeUntil } from 'rxjs/operators';
import {
noLeadTrailSpacesRegex,
RestSecurityType,
RestSecurityTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-rest-connector-security',
templateUrl: './rest-connector-security.component.html',
styleUrls: ['./rest-connector-security.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RestConnectorSecurityComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RestConnectorSecurityComponent),
multi: true
}
],
standalone: true,
imports: [
SharedModule,
CommonModule,
]
})
export class RestConnectorSecurityComponent implements ControlValueAccessor, Validator, OnDestroy {
BrokerSecurityType = RestSecurityType;
securityTypes: RestSecurityType[] = Object.values(RestSecurityType);
SecurityTypeTranslationsMap = RestSecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (_: any) => {};
constructor(private fb: FormBuilder) {
this.securityFormGroup = this.fb.group({
type: [RestSecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
});
this.observeSecurityForm();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any): void {
if (!deviceInfo.type) {
deviceInfo.type = RestSecurityType.ANONYMOUS;
}
this.securityFormGroup.reset(deviceInfo);
this.updateView(deviceInfo);
}
validate(): ValidationErrors | null {
return this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
private updateView(value: any): void {
this.propagateChange(value);
}
private updateValidators(type: RestSecurityType): void {
if (type === RestSecurityType.BASIC) {
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
} else {
this.securityFormGroup.get('username').disable({emitEvent: false});
this.securityFormGroup.get('password').disable({emitEvent: false});
}
}
private observeSecurityForm(): void {
this.securityFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateView(value));
this.securityFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => this.updateValidators(type));
}
}

21
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html

@ -44,7 +44,7 @@
<div class="fixed-title-width tb-required" translate>gateway.name</div> <div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput autocomplete="off" name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
@ -58,6 +58,22 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="connectorForm.get('type').value === connectorType.CUSTOM" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="class" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.GRPC" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div> <div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
@ -68,7 +84,8 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div *ngIf="connectorForm.get('type').value !== connectorType.GRPC && connectorForm.get('type').value !== connectorType.CUSTOM"
class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="useDefaults"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="useDefaults">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}"> <mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}">
{{ 'gateway.fill-connector-defaults' | translate }} {{ 'gateway.fill-connector-defaults' | translate }}

25
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -33,6 +33,7 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from "rxjs/operators";
@Component({ @Component({
selector: 'tb-add-connector-dialog', selector: 'tb-add-connector-dialog',
@ -40,7 +41,7 @@ import { ResourcesService } from '@core/services/resources.service';
styleUrls: ['./add-connector-dialog.component.scss'], styleUrls: ['./add-connector-dialog.component.scss'],
providers: [], providers: [],
}) })
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy { export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy {
connectorForm: UntypedFormGroup; connectorForm: UntypedFormGroup;
@ -66,9 +67,15 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
logLevel: [GatewayLogLevel.INFO, []], logLevel: [GatewayLogLevel.INFO, []],
useDefaults: [true, []], useDefaults: [true, []],
sendDataOnlyOnChange: [false, []], sendDataOnlyOnChange: [false, []],
class: ['', []],
key: ['auto', []],
}); });
} }
ngOnInit(): void {
this.observeTypeChange();
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
@ -118,4 +125,18 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
return null; return null;
}; };
} }
private observeTypeChange(): void {
this.connectorForm.get('type').valueChanges.pipe(
tap((type: ConnectorType) => {
const useDefaultControl = this.connectorForm.get('useDefaults');
if (type === ConnectorType.GRPC || type === ConnectorType.CUSTOM) {
useDefaultControl.setValue(false);
} else if (!useDefaultControl.value) {
useDefaultControl.setValue(true);
}
}),
takeUntil(this.destroy$),
).subscribe()
}
} }

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss

@ -33,7 +33,6 @@
} }
.mat-mdc-dialog-content { .mat-mdc-dialog-content {
max-height: 670px;
height: 670px; height: 670px;
} }

1113
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html

File diff suppressed because it is too large

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss

@ -55,7 +55,7 @@
} }
.statistics-container { .statistics-container {
width: 50%; width: 100%;
.command-container { .command-container {
width: 100%; width: 100%;

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -46,7 +46,7 @@
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
<mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)" <mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)"
(click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle> (click)="$event.stopPropagation(); onEnableConnector(attribute)"></mat-slide-toggle>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="key"> <ng-container matColumnDef="key">
@ -178,18 +178,36 @@
<div class="fixed-title-width tb-required" translate>gateway.name</div> <div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput autocomplete="off" name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate" [matTooltip]="(connectorForm.get('name').hasError('duplicateName') ?
*ngIf="connectorForm.get('name').hasError('required') && connectorForm.get('name').touched" 'gateway.connector-duplicate-name' : 'gateway.name-required') | translate"
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched) ||
connectorForm.get('name').hasError('duplicateName')"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="connectorForm.get('type').value === connectorType.CUSTOM" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="class" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.GRPC" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connectors-table-key</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.logs-configuration</div> <div class="tb-form-panel-title" translate>gateway.logs-configuration</div>
<div class="tb-form-row" fxLayoutAlign="space-between center"> <div class="tb-form-row" fxLayoutAlign="space-between center">

104
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -62,7 +62,7 @@ import {
} from './gateway-widget.models'; } from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { takeUntil } from 'rxjs/operators'; import { debounceTime, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
@ -148,6 +148,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private attributeUpdateSubject = new Subject<AttributeData>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: FormBuilder, private fb: FormBuilder,
@ -180,6 +181,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
basicConfig: this.fb.group({}) basicConfig: this.fb.group({})
}); });
this.connectorForm.disable(); this.connectorForm.disable();
this.observeAttributeChange();
} }
get portErrorTooltip(): string { get portErrorTooltip(): string {
@ -238,12 +241,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
if (sortHeaderId === 'syncStatus') { switch (sortHeaderId) {
return this.isConnectorSynced(data) ? 1 : 0; case 'syncStatus':
} else if (sortHeaderId === 'enabled') { return this.isConnectorSynced(data) ? 1 : 0;
return this.activeConnectors.includes(data.key) ? 1 : 0;
case 'enabled':
return this.activeConnectors.includes(data.key) ? 1 : 0;
case 'errors':
const errors = this.getErrorsCount(data);
if (typeof errors === 'string') {
return this.sort.direction.toUpperCase() === Direction.DESC ? -1 : Infinity;
}
return errors;
default:
return data[sortHeaderId] || data.value[sortHeaderId];
} }
return data[sortHeaderId] || data.value[sortHeaderId];
}; };
if (this.device) { if (this.device) {
@ -524,31 +538,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
enableConnector(attribute: AttributeData): void { onEnableConnector(attribute: AttributeData): void {
const wasEnabled = this.activeConnectors.includes(attribute.key);
const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
attribute.value.ts = new Date().getTime(); attribute.value.ts = new Date().getTime();
const tasks = [this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [{
key: 'active_connectors', this.updateActiveConnectorKeys(attribute.key);
value: this.activeConnectors
}]), this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, [{ this.attributeUpdateSubject.next(attribute);
key: 'inactive_connectors',
value: this.inactiveConnectors
}]), this.attributeService.deleteEntityAttributes(this.device, scopeOld, [attribute]),
this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute])];
if (wasEnabled) {
const index = this.activeConnectors.indexOf(attribute.key);
this.activeConnectors.splice(index, 1);
this.inactiveConnectors.push(attribute.key);
} else {
const index = this.inactiveConnectors.indexOf(attribute.key);
this.inactiveConnectors.splice(index, 1);
this.activeConnectors.push(attribute.key);
}
forkJoin(tasks).subscribe(_ => {
this.updateData(true);
});
} }
getErrorsCount(attribute: AttributeData): string { getErrorsCount(attribute: AttributeData): string {
@ -599,6 +594,55 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5)); this.connectorForm.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
} }
private observeAttributeChange(): void {
this.attributeUpdateSubject.pipe(
debounceTime(300),
tap((attribute: AttributeData) => this.executeAttributeUpdates(attribute)),
takeUntil(this.destroy$),
).subscribe();
}
private updateActiveConnectorKeys(key: string): void {
const wasEnabled = this.activeConnectors.includes(key);
if (wasEnabled) {
const index = this.activeConnectors.indexOf(key);
if (index !== -1) this.activeConnectors.splice(index, 1);
this.inactiveConnectors.push(key);
} else {
const index = this.inactiveConnectors.indexOf(key);
if (index !== -1) this.inactiveConnectors.splice(index, 1);
this.activeConnectors.push(key);
}
}
private executeAttributeUpdates(attribute: AttributeData): void {
forkJoin(this.getAttributeExecutionTasks(attribute))
.pipe(
take(1),
tap(() => this.updateData(true)),
takeUntil(this.destroy$),
)
.subscribe();
}
private getAttributeExecutionTasks(attribute: AttributeData): Observable<any>[] {
const isActive = this.activeConnectors.includes(attribute.key);
const scopeOld = isActive ? AttributeScope.SERVER_SCOPE : AttributeScope.SHARED_SCOPE;
const scopeNew = isActive ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
return [
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [{
key: 'active_connectors',
value: this.activeConnectors
}]),
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, [{
key: 'inactive_connectors',
value: this.inactiveConnectors
}]),
this.attributeService.deleteEntityAttributes(this.device, scopeOld, [attribute]),
this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute])];
}
private onDataUpdateError(e: any): void { private onDataUpdateError(e: any): void {
const exceptionData = this.utils.parseException(e); const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name; let errorText = exceptionData.name;

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html

@ -21,7 +21,7 @@
[active]="activeLink.name === link.name"> {{ link.name }} </a> [active]="activeLink.name === link.name"> {{ link.name }} </a>
</nav> </nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> <mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource" [trackBy]="trackByLogTs"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="ts"> <ng-container matColumnDef="ts">
@ -31,7 +31,7 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 10%">{{ 'widgets.gateway.status' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 10%">{{ 'widgets.gateway.level' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
<span [class]="statusClass(attribute.status)">{{ attribute.status }}</span> <span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</mat-cell> </mat-cell>

5
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts

@ -164,6 +164,10 @@ export class GatewayLogsComponent implements AfterViewInit {
} }
} }
trackByLogTs(_: number, log: GatewayLogData): number {
return log.ts;
}
private changeSubscription() { private changeSubscription() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) { if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{ this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
@ -178,5 +182,4 @@ export class GatewayLogsComponent implements AfterViewInit {
}; };
} }
} }
} }

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html

@ -18,8 +18,8 @@
<div class="mat-subtitle-1 title">{{ 'gateway.rpc.templates-title' | translate }}</div> <div class="mat-subtitle-1 title">{{ 'gateway.rpc.templates-title' | translate }}</div>
<mat-expansion-panel hideToggle *ngFor="let template of rpcTemplates"> <mat-expansion-panel hideToggle *ngFor="let template of rpcTemplates">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title class="template-name">
{{template.name}} <span matTooltip="{{template.name}}" matTooltipPosition="above">{{template.name}}</span>
</mat-panel-title> </mat-panel-title>
<mat-panel-description> <mat-panel-description>
<button mat-icon-button matTooltip="Delete" (click)="deleteTemplate($event, template)"> <button mat-icon-button matTooltip="Delete" (click)="deleteTemplate($event, template)">
@ -47,7 +47,10 @@
<div *ngIf="!isObject(config.value) else RPCObjectRow" <div *ngIf="!isObject(config.value) else RPCObjectRow"
[ngClass]="{'boolean-true': config.value === true, [ngClass]="{'boolean-true': config.value === true,
'boolean-false': config.value === false }"> 'boolean-false': config.value === false }">
{{config.value}}</div> <ng-container *ngIf="config.key === 'method' else value" [ngTemplateOutlet]="SNMPMethod"></ng-container>
</div>
<ng-template #value>{{ config.value }}</ng-template>
<ng-template #SNMPMethod>{{ SNMPMethodsTranslations.get(config.value) | translate }}</ng-template>
<ng-template #RPCObjectRow> <ng-template #RPCObjectRow>
<ng-container <ng-container
*ngFor="let subConfig of config.value | keyvalue : originalOrder" *ngFor="let subConfig of config.value | keyvalue : originalOrder"

15
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss

@ -59,6 +59,7 @@
flex-direction: row-reverse; flex-direction: row-reverse;
align-items: center; align-items: center;
margin-right: 0; margin-right: 0;
flex: 0;
& > mat-icon { & > mat-icon {
margin-left: 15px; margin-left: 15px;
@ -89,6 +90,20 @@
} }
} }
.template-name {
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
::ng-deep .mat-content {
align-items: center;
}
.mat-expansion-panel-header-title {
flex: 1;
margin: 0;
}
} }

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts

@ -17,7 +17,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { import {
ConnectorType, ConnectorType,
RPCTemplate RPCTemplate,
SNMPMethodsTranslations
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
@ -49,6 +50,7 @@ export class GatewayServiceRPCConnectorTemplatesComponent implements OnInit {
public readonly originalOrder = (): number => 0; public readonly originalOrder = (): number => 0;
public readonly isObject = (value: any) => isLiteralObject(value); public readonly isObject = (value: any) => isLiteralObject(value);
public readonly SNMPMethodsTranslations = SNMPMethodsTranslations;
constructor(private attributeService: AttributeService) { constructor(private attributeService: AttributeService) {
} }

36
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html

@ -368,38 +368,7 @@
{{ 'gateway.rpc.add-header' | translate }} {{ 'gateway.rpc.add-header' | translate }}
</button> </button>
</fieldset> </fieldset>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="security"> <tb-rest-connector-security [formControl]="commandForm.get('security')"></tb-rest-connector-security>
<span class="fields-label">{{ 'gateway.rpc.security' | translate }}</span>
<div class="border" fxLayout="column" fxLayoutGap="10px" *ngIf="getFormArrayControls('security').length">
<div fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center">
<span fxFlex class="title">{{ 'gateway.rpc.security-name' | translate }}</span>
<span fxFlex class="title">{{ 'gateway.rpc.value' | translate }}</span>
<span fxFlex="30px"></span>
</div>
<mat-divider></mat-divider>
<div fxFlex fxLayout="row" fxLayoutGap="10px" fxLayoutAlign="center center"
*ngFor="let control of getFormArrayControls('security'); let i = index">
<ng-container [formGroupName]="i">
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="securityName"/>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex>
<input matInput formControlName="value" placeholder="anonymous"/>
</mat-form-field>
<mat-icon style="cursor:pointer;"
fxFlex="30px"
(click)="removeHTTPSecurity(i)"
matTooltip="{{ 'gateway.rpc.remove' | translate }}">delete
</mat-icon>
</ng-container>
</div>
</div>
<button mat-raised-button
fxFlexAlign="start"
(click)="addHTTPSecurity()">
{{ 'gateway.rpc.add-security' | translate }}
</button>
</fieldset>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="ConnectorType.REQUEST"> <ng-template [ngSwitchCase]="ConnectorType.REQUEST">
<mat-form-field> <mat-form-field>
@ -478,9 +447,6 @@
</button> </button>
</fieldset> </fieldset>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA_ASYNCIO">
<ng-container *ngTemplateOutlet="OPCUAForm"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA" #OPCUAForm> <ng-template [ngSwitchCase]="ConnectorType.OPCUA" #OPCUAForm>
<mat-form-field > <mat-form-field >
<mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label> <mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label>

55
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormArray, FormArray,
@ -53,6 +53,8 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs";
@Component({ @Component({
selector: 'tb-gateway-service-rpc-connector', selector: 'tb-gateway-service-rpc-connector',
@ -66,7 +68,7 @@ import { deepClone } from '@core/utils';
} }
] ]
}) })
export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValueAccessor { export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, ControlValueAccessor {
@Input() @Input()
connectorType: ConnectorType; connectorType: ConnectorType;
@ -105,6 +107,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
private propagateChange = (v: any) => { private propagateChange = (v: any) => {
} }
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private dialog: MatDialog,) { private dialog: MatDialog,) {
@ -114,17 +117,12 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
this.commandForm = this.connectorParamsFormGroupByType(this.connectorType); this.commandForm = this.connectorParamsFormGroupByType(this.connectorType);
this.commandForm.valueChanges.subscribe(value => { this.commandForm.valueChanges.subscribe(value => {
const httpHeaders = {}; const httpHeaders = {};
const security = {};
switch (this.connectorType) { switch (this.connectorType) {
case ConnectorType.REST: case ConnectorType.REST:
value.httpHeaders.forEach(data => { value.httpHeaders.forEach(data => {
httpHeaders[data.headerName] = data.value; httpHeaders[data.headerName] = data.value;
}) })
value.httpHeaders = httpHeaders; value.httpHeaders = httpHeaders;
value.security.forEach(data => {
security[data.securityName] = data.value;
})
value.security = security;
break; break;
case ConnectorType.REQUEST: case ConnectorType.REQUEST:
value.httpHeaders.forEach(data => { value.httpHeaders.forEach(data => {
@ -138,6 +136,12 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
} }
}); });
this.isMQTTWithResponse = this.fb.control(false); this.isMQTTWithResponse = this.fb.control(false);
this.observeMQTTWithResponse();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
connectorParamsFormGroupByType(type: ConnectorType): FormGroup { connectorParamsFormGroupByType(type: ConnectorType): FormGroup {
@ -148,7 +152,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
formGroup = this.fb.group({ formGroup = this.fb.group({
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: [null, [Validators.pattern(noLeadTrailSpacesRegex)]], responseTopicExpression: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTimeout: [null, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]], responseTimeout: [null, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
}) })
@ -252,7 +256,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
tries: [null, [Validators.required, Validators.min(1), Validators.pattern(this.numbersOnlyPattern)]], tries: [null, [Validators.required, Validators.min(1), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
httpHeaders: this.fb.array([]), httpHeaders: this.fb.array([]),
security: this.fb.array([]) security: [{}, [Validators.required]]
}) })
break; break;
case ConnectorType.REQUEST: case ConnectorType.REQUEST:
@ -269,7 +273,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
}) })
break; break;
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
formGroup = this.fb.group({ formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
arguments: this.fb.array([]), arguments: this.fb.array([]),
@ -312,22 +315,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
oidsFA.removeAt(index); oidsFA.removeAt(index);
} }
addHTTPSecurity(value: { securityName: string, value: string } = {securityName: null, value: null}) {
const securityFA = this.commandForm.get('security') as FormArray;
const formGroup = this.fb.group({
securityName: [value.securityName, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value.value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
})
if (securityFA) {
securityFA.push(formGroup, {emitEvent: false});
}
}
removeHTTPSecurity(index: number) {
const oidsFA = this.commandForm.get('security') as FormArray;
oidsFA.removeAt(index);
}
getFormArrayControls(path: string) { getFormArrayControls(path: string) {
return (this.commandForm.get(path) as FormArray).controls as FormControl[]; return (this.commandForm.get(path) as FormArray).controls as FormControl[];
} }
@ -402,18 +389,12 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
break; break;
case ConnectorType.REST: case ConnectorType.REST:
this.clearFromArrayByName("httpHeaders"); this.clearFromArrayByName("httpHeaders");
this.clearFromArrayByName("security");
value.security && Object.entries(value.security).forEach(securityHeader => {
this.addHTTPSecurity({securityName: securityHeader[0], value: securityHeader[1] as string})
})
delete value.security;
value.httpHeaders && Object.entries(value.httpHeaders).forEach(httpHeader => { value.httpHeaders && Object.entries(value.httpHeaders).forEach(httpHeader => {
this.addHTTPHeader({headerName: httpHeader[0], value: httpHeader[1] as string}) this.addHTTPHeader({headerName: httpHeader[0], value: httpHeader[1] as string})
}) })
delete value.httpHeaders; delete value.httpHeaders;
break; break;
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
this.clearFromArrayByName("arguments"); this.clearFromArrayByName("arguments");
value.arguments.forEach(value => { value.arguments.forEach(value => {
this.addOCPUAArguments(value) this.addOCPUAArguments(value)
@ -424,4 +405,14 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
this.commandForm.patchValue(value, {onlySelf: false}); this.commandForm.patchValue(value, {onlySelf: false});
} }
} }
private observeMQTTWithResponse(): void {
this.isMQTTWithResponse.valueChanges.pipe(
tap((isActive: boolean) => {
const responseControl = this.commandForm.get('responseTopicExpression');
isActive ? responseControl.enable() : responseControl.disable();
}),
takeUntil(this.destroy$),
).subscribe();
}
} }

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -150,7 +150,6 @@ export class GatewayServiceRPCComponent implements OnInit {
case ConnectorType.BACNET: case ConnectorType.BACNET:
case ConnectorType.CAN: case ConnectorType.CAN:
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
return params.method; return params.method;
case ConnectorType.BLE: case ConnectorType.BLE:
case ConnectorType.OCPP: case ConnectorType.OCPP:

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -125,7 +125,6 @@ export enum ConnectorType {
MODBUS = 'modbus', MODBUS = 'modbus',
GRPC = 'grpc', GRPC = 'grpc',
OPCUA = 'opcua', OPCUA = 'opcua',
OPCUA_ASYNCIO = 'opcua_asyncio',
BLE = 'ble', BLE = 'ble',
REQUEST = 'request', REQUEST = 'request',
CAN = 'can', CAN = 'can',
@ -145,7 +144,6 @@ export const GatewayConnectorDefaultTypesTranslatesMap = new Map<ConnectorType,
[ConnectorType.MODBUS, 'MODBUS'], [ConnectorType.MODBUS, 'MODBUS'],
[ConnectorType.GRPC, 'GRPC'], [ConnectorType.GRPC, 'GRPC'],
[ConnectorType.OPCUA, 'OPCUA'], [ConnectorType.OPCUA, 'OPCUA'],
[ConnectorType.OPCUA_ASYNCIO, 'OPCUA ASYNCIO'],
[ConnectorType.BLE, 'BLE'], [ConnectorType.BLE, 'BLE'],
[ConnectorType.REQUEST, 'REQUEST'], [ConnectorType.REQUEST, 'REQUEST'],
[ConnectorType.CAN, 'CAN'], [ConnectorType.CAN, 'CAN'],
@ -269,7 +267,7 @@ export const SNMPMethodsTranslations = new Map<SNMPMethods, string>([
[SNMPMethods.TABLE, 'gateway.rpc.table'], [SNMPMethods.TABLE, 'gateway.rpc.table'],
[SNMPMethods.MULTIGET, 'gateway.rpc.multi-get'], [SNMPMethods.MULTIGET, 'gateway.rpc.multi-get'],
[SNMPMethods.GETNEXT, 'gateway.rpc.get-next'], [SNMPMethods.GETNEXT, 'gateway.rpc.get-next'],
[SNMPMethods.BULKGET, 'gateway.rpc.bul-kget'], [SNMPMethods.BULKGET, 'gateway.rpc.bulk-get'],
[SNMPMethods.WALKS, 'gateway.rpc.walk'] [SNMPMethods.WALKS, 'gateway.rpc.walk']
]) ])
@ -360,6 +358,18 @@ export const BrokerSecurityTypeTranslationsMap = new Map<BrokerSecurityType, str
] ]
); );
export enum RestSecurityType {
ANONYMOUS = 'anonymous',
BASIC = 'basic',
}
export const RestSecurityTypeTranslationsMap = new Map<RestSecurityType, string>(
[
[RestSecurityType.ANONYMOUS, 'gateway.broker.security-types.anonymous'],
[RestSecurityType.BASIC, 'gateway.broker.security-types.basic'],
]
);
export const MqttVersions = [ export const MqttVersions = [
{ name: 3.1, value: 3 }, { name: 3.1, value: 3 },
{ name: 3.11, value: 4 }, { name: 3.11, value: 4 },

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

Loading…
Cancel
Save