Browse Source

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

pull/11112/head
ViacheslavKlimov 2 years ago
parent
commit
50b2d0e488
  1. 199
      application/src/main/data/json/tenant/dashboards/gateways.json
  2. 14
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  3. 18
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  4. 77
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  5. 10
      application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
  6. 3
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeInstallInstructionsService.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  10. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/telemetry/EntityDataMsgConstructor.java
  11. 17
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV2.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProcessor.java
  13. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  15. 12
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  16. 10
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
  17. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java
  18. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  19. 478
      application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
  20. 817
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  21. 1005
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  22. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java
  23. 25
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  24. 4
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  25. 1
      common/proto/src/main/proto/queue.proto
  26. 2
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  27. 50
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  28. 60
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  29. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  30. 1
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  31. 3
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html
  32. 45
      ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts
  33. 46
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  34. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  35. 14
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts
  36. 38
      ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts
  37. 8
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts
  38. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table.component.html
  39. 65
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.html
  40. 29
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.scss
  41. 132
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.ts
  42. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html
  43. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  44. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  45. 1263
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  46. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  47. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  48. 104
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  49. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  50. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  51. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.html
  52. 15
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.scss
  53. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component.ts
  54. 36
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  55. 55
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  56. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  57. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  58. 18
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  59. 1
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  60. 2
      ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts
  61. 1
      ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html
  62. 11
      ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html
  63. 4
      ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts
  64. 11
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html
  65. 3
      ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts
  66. 13
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  67. 17
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  68. 11
      ui-ngx/src/app/shared/components/entity/entity-list.component.html
  69. 4
      ui-ngx/src/app/shared/components/entity/entity-list.component.ts
  70. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  71. 1
      ui-ngx/src/assets/locale/locale.constant-lt_LT.json
  72. 1
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json

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

@ -248,7 +248,7 @@
"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",
"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": [],
"openInSeparateDialog": false,
"openInPopover": false,
@ -880,7 +880,7 @@
"isConnectorLog": true,
"connectorLogState": "connector_logs"
},
"title": "Gateway Logs",
"title": "${entityName} Logs",
"showTitleIcon": false,
"dropShadow": true,
"enableFullscreen": true,
@ -2134,7 +2134,7 @@
"padding": "0px",
"settings": {
"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,
"markdownCss": ".mat-mdc-form-field-subscript-wrapper {\n display: none !important;\n}"
},
@ -2996,172 +2996,6 @@
"id": "bb27723a-989c-2327-5808-b56d490b93ab",
"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": {
"type": "latest",
"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": {
"name": "gateway_devices_ble",
"root": false,

14
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -270,14 +270,16 @@ public class TenantActor extends RuleChainManagerActor {
initRuleChains();
}
}
if (msg.getEntityId().getEntityType() == EntityType.EDGE) {
if (msg.getEntityId().getEntityType() == EntityType.EDGE && isCore) {
EdgeId edgeId = new EdgeId(msg.getEntityId().getId());
EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService();
if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {
edgeRpcService.deleteEdge(tenantId, edgeId);
} else if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) {
Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId);
edgeRpcService.updateEdge(tenantId, edge);
if (edgeRpcService != null) {
if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {
edgeRpcService.deleteEdge(tenantId, edgeId);
} else if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) {
Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId);
edgeRpcService.updateEdge(tenantId, edge);
}
}
}
if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent() && isMyPartition(msg.getEntityId())) {

18
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -29,16 +29,13 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -120,6 +117,9 @@ public class AdminController extends BaseController {
private final SystemInfoService systemInfoService;
private final AuditLogService auditLogService;
@Value("${queue.vc.request-timeout:180000}")
private int vcRequestTimeout;
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@ -301,13 +301,14 @@ public class AdminController extends BaseController {
@PostMapping("/repositorySettings")
public DeferredResult<RepositorySettings> saveRepositorySettings(@RequestBody RepositorySettings settings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
settings.setLocalOnly(false); // only to be used in tests
ListenableFuture<RepositorySettings> future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
return wrapFuture(Futures.transform(future, savedSettings -> {
savedSettings.setPassword(null);
savedSettings.setPrivateKey(null);
savedSettings.setPrivateKeyPassword(null);
return savedSettings;
}, MoreExecutors.directExecutor()));
}, MoreExecutors.directExecutor()), vcRequestTimeout);
}
@ApiOperation(value = "Delete repository settings (deleteRepositorySettings)",
@ -318,7 +319,7 @@ public class AdminController extends BaseController {
@ResponseStatus(value = HttpStatus.OK)
public DeferredResult<Void> deleteRepositorySettings() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()), vcRequestTimeout);
}
@ApiOperation(value = "Check repository access (checkRepositoryAccess)",
@ -329,8 +330,8 @@ public class AdminController extends BaseController {
@Parameter(description = "A JSON value representing the Repository Settings.")
@RequestBody RepositorySettings settings) throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
settings = checkNotNull(settings);
return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings));
settings.setLocalOnly(false); // only to be used in tests
return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings), vcRequestTimeout);
}
@ApiOperation(value = "Get auto commit settings (getAutoCommitSettings)",
@ -482,4 +483,5 @@ public class AdminController extends BaseController {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
response.sendRedirect(prevUri);
}
}

77
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -18,24 +18,20 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
@ -114,8 +110,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Is edges support enabled (isEdgesSupportEnabled)",
notes = "Returns 'true' if edges support enabled on server, 'false' - otherwise.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edges/enabled", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edges/enabled")
public boolean isEdgesSupportEnabled() {
return edgesEnabled;
}
@ -123,8 +118,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Get Edge (getEdgeById)",
notes = "Get the Edge object based on the provided Edge Id. " + EDGE_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}")
public Edge getEdgeById(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -135,8 +129,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Get Edge Info (getEdgeInfoById)",
notes = "Get the Edge Info object based on the provided Edge Id. " + EDGE_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/info/{edgeId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/info/{edgeId}")
public EdgeInfo getEdgeInfoById(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -153,8 +146,7 @@ public class EdgeController extends BaseController {
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Edge entity. " +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/edge")
public Edge saveEdge(@Parameter(description = "A JSON value representing the edge.", required = true)
@RequestBody Edge edge) throws Exception {
TenantId tenantId = getTenantId();
@ -179,8 +171,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Delete edge (deleteEdge)",
notes = "Deletes the edge. Referencing non-existing edge Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
@DeleteMapping(value = "/edge/{edgeId}")
public void deleteEdge(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -193,8 +184,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edges", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edges", params = {"pageSize", "page"})
public PageData<Edge> getEdges(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -213,8 +203,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Assign edge to customer (assignEdgeToCustomer)",
notes = "Creates assignment of the edge to customer. Customer will be able to query edge afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/edge/{edgeId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/customer/{customerId}/edge/{edgeId}")
public Edge assignEdgeToCustomer(@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("customerId") String strCustomerId,
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@ -231,8 +220,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Unassign edge from customer (unassignEdgeFromCustomer)",
notes = "Clears assignment of the edge to customer. Customer will not be able to query edge afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/edge/{edgeId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/customer/edge/{edgeId}")
public Edge unassignEdgeFromCustomer(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -251,8 +239,7 @@ public class EdgeController extends BaseController {
"This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
"However, users that are logged-in and belong to different tenant will not be able to access the edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/edge/{edgeId}", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/customer/public/edge/{edgeId}")
public Edge assignEdgeToPublicCustomer(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
@ -265,8 +252,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/edges", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/edges", params = {"pageSize", "page"})
public PageData<Edge> getTenantEdges(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -293,8 +279,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"})
public PageData<EdgeInfo> getTenantEdgeInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -321,8 +306,7 @@ public class EdgeController extends BaseController {
notes = "Requested edge must be owned by tenant or customer that the user belongs to. " +
"Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/edges", params = {"edgeName"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/edges", params = {"edgeName"})
public Edge getTenantEdge(@Parameter(description = "Unique name of the edge", required = true)
@RequestParam String edgeName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
@ -333,8 +317,7 @@ public class EdgeController extends BaseController {
notes = "Change root rule chain of the edge to the new provided rule chain. \n" +
"This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/{ruleChainId}/root", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/edge/{edgeId}/{ruleChainId}/root")
public Edge setEdgeRootRuleChain(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION, required = true)
@ -353,8 +336,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"})
public PageData<Edge> getCustomerEdges(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -389,8 +371,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"})
public PageData<EdgeInfo> getCustomerEdgeInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -424,8 +405,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Get Edges By Ids (getEdgesByIds)",
notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edges", params = {"edgeIds"})
public List<Edge> getEdgesByIds(
@Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException {
@ -452,8 +432,7 @@ public class EdgeController extends BaseController {
"The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edges", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/edges")
public List<Edge> findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
checkNotNull(query.getParameters());
@ -477,8 +456,7 @@ public class EdgeController extends BaseController {
notes = "Returns a set of unique edge types based on edges that are either owned by the tenant or assigned to the customer which user is performing the request."
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/types", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/types")
public List<EntitySubtype> getEdgeTypes() throws ThingsboardException, ExecutionException, InterruptedException {
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
@ -490,7 +468,7 @@ public class EdgeController extends BaseController {
notes = "Starts synchronization process between edge and cloud. \n" +
"All entities that are assigned to particular edge are going to be send to remote edge service." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/sync/{edgeId}", method = RequestMethod.POST)
@PostMapping(value = "/edge/sync/{edgeId}")
public DeferredResult<ResponseEntity> syncEdge(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId);
@ -519,8 +497,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Find missing rule chains (findMissingToRelatedRuleChains)",
notes = "Returns list of rule chains ids that are not assigned to particular edge, but these rule chains are present in the already assigned rule chains to edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/missingToRelatedRuleChains/{edgeId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/missingToRelatedRuleChains/{edgeId}")
public String findMissingToRelatedRuleChains(@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
@ -547,8 +524,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Get Edge Install Instructions (getEdgeInstallInstructions)",
notes = "Get an install instructions for provided edge id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/instructions/install/{edgeId}/{method}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/instructions/install/{edgeId}/{method}")
public EdgeInstructions getEdgeInstallInstructions(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId,
@ -568,8 +544,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Get Edge Upgrade Instructions (getEdgeUpgradeInstructions)",
notes = "Get an upgrade instructions for provided edge version." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/instructions/upgrade/{edgeVersion}/{method}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/instructions/upgrade/{edgeVersion}/{method}")
public EdgeInstructions getEdgeUpgradeInstructions(
@Parameter(description = "Edge version", required = true)
@PathVariable("edgeVersion") String edgeVersion,
@ -585,8 +560,7 @@ public class EdgeController extends BaseController {
@ApiOperation(value = "Is edge upgrade enabled (isEdgeUpgradeAvailable)",
notes = "Returns 'true' if upgrade available for connected edge, 'false' - otherwise.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/upgrade/available", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}/upgrade/available")
public boolean isEdgeUpgradeAvailable(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId) throws Exception {
@ -599,4 +573,5 @@ public class EdgeController extends BaseController {
throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL);
}
}
}

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

@ -16,18 +16,14 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -62,8 +58,7 @@ public class EdgeEventController extends BaseController {
notes = "Returns a page of edge events for the requested edge. " +
PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/events", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}/events")
public PageData<EdgeEvent> getEdgeEvents(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,
@ -88,4 +83,5 @@ public class EdgeEventController extends BaseController {
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink));
}
}

3
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -137,6 +137,9 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists();
systemDataLoaderService.updateDefaultNotificationConfigs(false);
systemDataLoaderService.updateSecuritySettings();
case "3.7.0":
log.info("Upgrading ThingsBoard from version 3.7.0 to 3.7.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.7.0");
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:

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

@ -15,9 +15,7 @@
*/
package org.thingsboard.server.service.edge.instructions;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AttributeScope;
@ -164,4 +162,5 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
protected String getBaseDirName() {
return UPGRADE_DIR;
}
}

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

@ -15,14 +15,14 @@
*/
package org.thingsboard.server.service.edge.instructions;
import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInstructions;
import jakarta.servlet.http.HttpServletRequest;
public interface EdgeInstallInstructionsService {
EdgeInstructions getInstallInstructions(Edge edge, String installationMethod, HttpServletRequest request);
void setAppVersion(String version);
}

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

@ -21,6 +21,9 @@ import com.google.common.util.concurrent.Futures;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.stub.StreamObserver;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -55,9 +58,6 @@ import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/telemetry/EntityDataMsgConstructor.java

@ -23,12 +23,12 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg;
import org.thingsboard.server.gen.edge.v1.EntityDataProto;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -106,4 +106,5 @@ public class EntityDataMsgConstructor {
}
return result;
}
}

17
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV2.java

@ -19,7 +19,6 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
@ -37,22 +36,6 @@ public class TenantMsgConstructorV2 implements TenantMsgConstructor {
@Override
public TenantProfileUpdateMsg constructTenantProfileUpdateMsg(UpdateMsgType msgType, TenantProfile tenantProfile, EdgeVersion edgeVersion) {
tenantProfile = JacksonUtil.clone(tenantProfile);
// clear all config
var configuration = tenantProfile.getDefaultProfileConfiguration();
configuration.setRpcTtlDays(0);
configuration.setMaxJSExecutions(0);
configuration.setMaxREExecutions(0);
configuration.setMaxDPStorageDays(0);
configuration.setMaxTbelExecutions(0);
configuration.setQueueStatsTtlDays(0);
configuration.setMaxTransportMessages(0);
configuration.setDefaultStorageTtlDays(0);
configuration.setMaxTransportDataPoints(0);
configuration.setRuleEngineExceptionsTtlDays(0);
configuration.setMaxRuleNodeExecutionsPerMessage(0);
tenantProfile.getProfileData().setConfiguration(configuration);
return TenantProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenantProfile)).build();
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProcessor.java

@ -21,7 +21,6 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
@ -37,4 +36,5 @@ public interface DeviceProcessor extends EdgeProcessor {
DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion);
ListenableFuture<Void> processDeviceRpcCallFromEdge(TenantId tenantId, Edge edge, DeviceRpcCallMsg deviceRpcCallMsg);
}

4
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -121,6 +121,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
case "3.6.4":
updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null);
break;
case "3.7.0":
updateSchema("3.7.0", 3007000, "3.7.1", 3007001, null);
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
@ -189,4 +192,5 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
}
return isOldSchema;
}
}

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

@ -165,7 +165,7 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void pushMsgToVersionControl(TenantId tenantId, TransportProtos.ToVersionControlServiceMsg msg, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_VC_EXECUTOR, TenantId.SYS_TENANT_ID, tenantId);
log.trace("PUSHING msg: {} to:{}", msg, tpi);
producerProvider.getTbVersionControlMsgProducer().send(tpi, new TbProtoQueueMsg<>(tenantId.getId(), msg), callback);
//TODO: ashvayka

12
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -164,13 +164,13 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
var cacheEntry = taskCache.get(requestId);
if (cacheEntry == null || cacheEntry.get() == null) {
log.debug("[{}] No cache record: {}", requestId, cacheEntry);
throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
throw new ThingsboardException("Task execution timed-out", ThingsboardErrorCode.ITEM_NOT_FOUND);
} else {
var entry = cacheEntry.get();
log.trace("[{}] Cache get: {}", requestId, entry);
var result = getter.apply(entry);
if (result == null) {
throw new ThingsboardException(ThingsboardErrorCode.BAD_REQUEST_PARAMS);
throw new ThingsboardException("Invalid task", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return result;
}
@ -526,12 +526,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
@Override
public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception {
if (repositorySettingsService.delete(tenantId)) {
return gitServiceQueue.clearRepository(tenantId);
} else {
return Futures.immediateFuture(null);
}
public ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) {
return gitServiceQueue.clearRepository(tenantId);
}
@Override

10
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -311,7 +312,13 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
}
return submitFuture;
} else {
throw new RuntimeException("Future is already done!");
try {
request.getFuture().get();
throw new RuntimeException("Failed to process the request");
} catch (Exception e) {
Throwable cause = ExceptionUtils.getRootCause(e);
throw new RuntimeException(cause.getMessage(), cause);
}
}
}
@ -562,5 +569,6 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
private CommitRequestMsg.Builder buildCommitRequest(CommitGitRequest commit) {
return CommitRequestMsg.newBuilder().setTxId(commit.getTxId().toString());
}
}

2
application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java

@ -65,7 +65,7 @@ public interface EntitiesVersionControlService {
ListenableFuture<RepositorySettings> saveVersionControlSettings(TenantId tenantId, RepositorySettings versionControlSettings);
ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId) throws Exception;
ListenableFuture<Void> deleteVersionControlSettings(TenantId tenantId);
ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception;

2
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -803,7 +803,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseType);
}
protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
protected <T, R> R doPostAsync(String urlTemplate, T content, Class<R> responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);
}

478
application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java

@ -1,478 +0,0 @@
/**
* 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.sync.ie;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.After;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
import org.thingsboard.server.service.sync.vc.data.SimpleEntitiesExportCtx;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
public abstract class BaseExportImportServiceTest extends AbstractControllerTest {
@Autowired
protected EntitiesExportImportService exportImportService;
@Autowired
protected DeviceService deviceService;
@Autowired
protected OtaPackageService otaPackageService;
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected AssetService assetService;
@Autowired
protected CustomerService customerService;
@Autowired
protected RuleChainService ruleChainService;
@Autowired
protected DashboardService dashboardService;
@Autowired
protected RelationService relationService;
@Autowired
protected TenantService tenantService;
@Autowired
protected EntityViewService entityViewService;
protected TenantId tenantId1;
protected User tenantAdmin1;
protected TenantId tenantId2;
protected User tenantAdmin2;
@Before
public void beforeEach() throws Exception {
loginSysAdmin();
Tenant tenant1 = new Tenant();
tenant1.setTitle("Tenant 1");
tenant1.setEmail("tenant1@thingsboard.org");
this.tenantId1 = tenantService.saveTenant(tenant1).getId();
User tenantAdmin1 = new User();
tenantAdmin1.setTenantId(tenantId1);
tenantAdmin1.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin1.setEmail("tenant1-admin@thingsboard.org");
this.tenantAdmin1 = createUser(tenantAdmin1, "12345678");
Tenant tenant2 = new Tenant();
tenant2.setTitle("Tenant 2");
tenant2.setEmail("tenant2@thingsboard.org");
this.tenantId2 = tenantService.saveTenant(tenant2).getId();
User tenantAdmin2 = new User();
tenantAdmin2.setTenantId(tenantId2);
tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin2.setEmail("tenant2-admin@thingsboard.org");
this.tenantAdmin2 = createUser(tenantAdmin2, "12345678");
}
@After
public void afterEach() {
tenantService.deleteTenant(tenantId1);
tenantService.deleteTenant(tenantId2);
}
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
Device device = new Device();
device.setTenantId(tenantId);
device.setCustomerId(customerId);
device.setName(name);
device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId);
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
device.setDeviceData(deviceData);
return deviceService.saveDevice(device);
}
protected OtaPackage createOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType type) {
OtaPackage otaPackage = new OtaPackage();
otaPackage.setTenantId(tenantId);
otaPackage.setDeviceProfileId(deviceProfileId);
otaPackage.setType(type);
otaPackage.setTitle("My " + type);
otaPackage.setVersion("v1.0");
otaPackage.setFileName("filename.txt");
otaPackage.setContentType("text/plain");
otaPackage.setChecksumAlgorithm(ChecksumAlgorithm.SHA256);
otaPackage.setChecksum("4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a");
otaPackage.setDataSize(1L);
otaPackage.setData(ByteBuffer.wrap(new byte[]{(int) 1}));
return otaPackageService.saveOtaPackage(otaPackage);
}
protected void checkImportedDeviceData(Device initialDevice, Device importedDevice) {
assertThat(importedDevice.getName()).isEqualTo(initialDevice.getName());
assertThat(importedDevice.getType()).isEqualTo(initialDevice.getType());
assertThat(importedDevice.getDeviceData()).isEqualTo(initialDevice.getDeviceData());
assertThat(importedDevice.getLabel()).isEqualTo(initialDevice.getLabel());
}
protected DeviceProfile createDeviceProfile(TenantId tenantId, RuleChainId defaultRuleChainId, DashboardId defaultDashboardId, String name) {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);
deviceProfile.setName(name);
deviceProfile.setDescription("dscrptn");
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
deviceProfile.setDefaultRuleChainId(defaultRuleChainId);
deviceProfile.setDefaultDashboardId(defaultDashboardId);
DeviceProfileData profileData = new DeviceProfileData();
profileData.setConfiguration(new DefaultDeviceProfileConfiguration());
profileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration());
deviceProfile.setProfileData(profileData);
return deviceProfileService.saveDeviceProfile(deviceProfile);
}
protected void checkImportedDeviceProfileData(DeviceProfile initialProfile, DeviceProfile importedProfile) {
assertThat(initialProfile.getName()).isEqualTo(importedProfile.getName());
assertThat(initialProfile.getType()).isEqualTo(importedProfile.getType());
assertThat(initialProfile.getTransportType()).isEqualTo(importedProfile.getTransportType());
assertThat(initialProfile.getProfileData()).isEqualTo(importedProfile.getProfileData());
assertThat(initialProfile.getDescription()).isEqualTo(importedProfile.getDescription());
}
protected AssetProfile createAssetProfile(TenantId tenantId, RuleChainId defaultRuleChainId, DashboardId defaultDashboardId, String name) {
AssetProfile assetProfile = new AssetProfile();
assetProfile.setTenantId(tenantId);
assetProfile.setName(name);
assetProfile.setDescription("dscrptn");
assetProfile.setDefaultRuleChainId(defaultRuleChainId);
assetProfile.setDefaultDashboardId(defaultDashboardId);
return assetProfileService.saveAssetProfile(assetProfile);
}
protected void checkImportedAssetProfileData(AssetProfile initialProfile, AssetProfile importedProfile) {
assertThat(initialProfile.getName()).isEqualTo(importedProfile.getName());
assertThat(initialProfile.getDescription()).isEqualTo(importedProfile.getDescription());
}
protected Asset createAsset(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, String name) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setCustomerId(customerId);
asset.setAssetProfileId(assetProfileId);
asset.setName(name);
asset.setLabel("lbl");
asset.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
return assetService.saveAsset(asset);
}
protected void checkImportedAssetData(Asset initialAsset, Asset importedAsset) {
assertThat(importedAsset.getName()).isEqualTo(initialAsset.getName());
assertThat(importedAsset.getType()).isEqualTo(initialAsset.getType());
assertThat(importedAsset.getLabel()).isEqualTo(initialAsset.getLabel());
assertThat(importedAsset.getAdditionalInfo()).isEqualTo(initialAsset.getAdditionalInfo());
}
protected Customer createCustomer(TenantId tenantId, String name) {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle(name);
customer.setCountry("ua");
customer.setAddress("abb");
customer.setEmail("ccc@aa.org");
customer.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
return customerService.saveCustomer(customer);
}
protected void checkImportedCustomerData(Customer initialCustomer, Customer importedCustomer) {
assertThat(importedCustomer.getTitle()).isEqualTo(initialCustomer.getTitle());
assertThat(importedCustomer.getCountry()).isEqualTo(initialCustomer.getCountry());
assertThat(importedCustomer.getAddress()).isEqualTo(initialCustomer.getAddress());
assertThat(importedCustomer.getEmail()).isEqualTo(initialCustomer.getEmail());
}
protected Dashboard createDashboard(TenantId tenantId, CustomerId customerId, String name) {
Dashboard dashboard = new Dashboard();
dashboard.setTenantId(tenantId);
dashboard.setTitle(name);
dashboard.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
dashboard.setImage("abvregewrg");
dashboard.setMobileHide(true);
dashboard = dashboardService.saveDashboard(dashboard);
if (customerId != null) {
dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerId);
return dashboardService.findDashboardById(tenantId, dashboard.getId());
}
return dashboard;
}
protected Dashboard createDashboard(TenantId tenantId, CustomerId customerId, String name, AssetId assetForEntityAlias) {
Dashboard dashboard = createDashboard(tenantId, customerId, name);
String entityAliases = "{\n" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" +
"\t\t\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" +
"\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" +
"\t\t\t],\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" +
"\t\t\t\"type\": \"entityList\"\n" +
"\t\t},\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" +
"\t}\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("description", new TextNode("hallo"));
dashboard.setConfiguration(dashboardConfiguration);
return dashboardService.saveDashboard(dashboard);
}
protected void checkImportedDashboardData(Dashboard initialDashboard, Dashboard importedDashboard) {
assertThat(importedDashboard.getTitle()).isEqualTo(initialDashboard.getTitle());
assertThat(importedDashboard.getConfiguration()).isEqualTo(initialDashboard.getConfiguration());
assertThat(importedDashboard.getImage()).isEqualTo(initialDashboard.getImage());
assertThat(importedDashboard.isMobileHide()).isEqualTo(initialDashboard.isMobileHide());
if (initialDashboard.getAssignedCustomers() != null) {
assertThat(importedDashboard.getAssignedCustomers()).containsAll(initialDashboard.getAssignedCustomers());
}
}
protected RuleChain createRuleChain(TenantId tenantId, String name, EntityId originatorId) {
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName(name);
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Generator 1");
ruleNode1.setType(TbMsgGeneratorNode.class.getName());
ruleNode1.setDebugMode(true);
TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration();
configuration1.setOriginatorType(originatorId.getEntityType());
configuration1.setOriginatorId(originatorId.getId().toString());
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
protected RuleChain createRuleChain(TenantId tenantId, String name) {
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName(name);
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) {
assertThat(importedRuleChain.getType()).isEqualTo(initialRuleChain.getType());
assertThat(importedRuleChain.getName()).isEqualTo(initialRuleChain.getName());
assertThat(importedRuleChain.isDebugMode()).isEqualTo(initialRuleChain.isDebugMode());
assertThat(importedRuleChain.getConfiguration()).isEqualTo(initialRuleChain.getConfiguration());
assertThat(importedMetaData.getConnections()).isEqualTo(initialMetaData.getConnections());
assertThat(importedMetaData.getFirstNodeIndex()).isEqualTo(initialMetaData.getFirstNodeIndex());
for (int i = 0; i < initialMetaData.getNodes().size(); i++) {
RuleNode initialNode = initialMetaData.getNodes().get(i);
RuleNode importedNode = importedMetaData.getNodes().get(i);
assertThat(importedNode.getRuleChainId()).isEqualTo(importedRuleChain.getId());
assertThat(importedNode.getName()).isEqualTo(initialNode.getName());
assertThat(importedNode.getType()).isEqualTo(initialNode.getType());
assertThat(importedNode.getConfiguration()).isEqualTo(initialNode.getConfiguration());
assertThat(importedNode.getAdditionalInfo()).isEqualTo(initialNode.getAdditionalInfo());
}
}
protected EntityView createEntityView(TenantId tenantId, CustomerId customerId, EntityId entityId, String name) {
EntityView entityView = new EntityView();
entityView.setTenantId(tenantId);
entityView.setEntityId(entityId);
entityView.setCustomerId(customerId);
entityView.setName(name);
entityView.setType("A");
return entityViewService.saveEntityView(entityView);
}
protected EntityRelation createRelation(EntityId from, EntityId to) {
EntityRelation relation = new EntityRelation();
relation.setFrom(from);
relation.setTo(to);
relation.setType(EntityRelation.MANAGES_TYPE);
relation.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
relation.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(TenantId.SYS_TENANT_ID, relation);
return relation;
}
protected <E extends ExportableEntity<?> & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) {
assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1);
assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2);
assertThat(importedEntity.getExternalId()).isEqualTo(initialEntity.getId());
boolean sameTenant = tenantId1.equals(tenantId2);
if (!sameTenant) {
assertThat(importedEntity.getId()).isNotEqualTo(initialEntity.getId());
} else {
assertThat(importedEntity.getId()).isEqualTo(initialEntity.getId());
}
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId) throws Exception {
return exportEntity(user, entityId, EntityExportSettings.builder()
.exportCredentials(true)
.build());
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId, EntityExportSettings exportSettings) throws Exception {
return exportImportService.exportEntity(new SimpleEntitiesExportCtx(getSecurityUser(user), null, null, exportSettings), entityId);
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData) throws Exception {
return importEntity(user, exportData, EntityImportSettings.builder()
.saveCredentials(true)
.build());
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData, EntityImportSettings importSettings) throws Exception {
EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), getSecurityUser(user), null, importSettings);
ctx.setFinalImportAttempt(true);
exportData = JacksonUtil.treeToValue(JacksonUtil.valueToTree(exportData), EntityExportData.class);
EntityImportResult<E> importResult = exportImportService.importEntity(ctx, exportData);
exportImportService.saveReferencesAndRelations(ctx);
for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) {
throwingRunnable.run();
}
return importResult;
}
protected SecurityUser getSecurityUser(User user) {
return new SecurityUser(user, true, new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()));
}
}

817
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -15,10 +15,10 @@
*/
package org.thingsboard.server.service.sync.ie;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.collect.Streams;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,19 +26,32 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -46,27 +59,46 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.sync.ie.DeviceExportData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import java.util.ArrayList;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
import org.thingsboard.server.service.sync.vc.data.SimpleEntitiesExportCtx;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -76,481 +108,77 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
@DaoSqlTest
public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
public class ExportImportServiceSqlTest extends AbstractControllerTest {
@Autowired
private DeviceCredentialsService deviceCredentialsService;
@SpyBean
private EntityActionService entityActionService;
@SpyBean
private OtaPackageStateService otaPackageStateService;
@Test
public void testExportImportAssetWithProfile_betweenTenants() throws Exception {
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile of tenant 1");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset of tenant 1");
EntityExportData<AssetProfile> profileExportData = exportEntity(tenantAdmin1, assetProfile.getId());
EntityExportData<Asset> assetExportData = exportEntity(tenantAdmin1, asset.getId());
EntityImportResult<AssetProfile> profileImportResult = importEntity(tenantAdmin2, profileExportData);
checkImportedEntity(tenantId1, assetProfile, tenantId2, profileImportResult.getSavedEntity());
checkImportedAssetProfileData(assetProfile, profileImportResult.getSavedEntity());
EntityImportResult<Asset> assetImportResult = importEntity(tenantAdmin2, assetExportData);
Asset importedAsset = assetImportResult.getSavedEntity();
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
checkImportedAssetData(asset, importedAsset);
assertThat(importedAsset.getAssetProfileId()).isEqualTo(profileImportResult.getSavedEntity().getId());
}
@Test
public void testExportImportAsset_sameTenant() throws Exception {
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile v1.0");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset v1.0");
EntityExportData<Asset> exportData = exportEntity(tenantAdmin1, asset.getId());
EntityImportResult<Asset> importResult = importEntity(tenantAdmin1, exportData);
checkImportedEntity(tenantId1, asset, tenantId1, importResult.getSavedEntity());
checkImportedAssetData(asset, importResult.getSavedEntity());
}
@Test
public void testExportImportAsset_sameTenant_withCustomer() throws Exception {
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile v1.0");
Customer customer = createCustomer(tenantId1, "My customer");
Asset asset = createAsset(tenantId1, customer.getId(), assetProfile.getId(), "My asset");
Asset importedAsset = importEntity(tenantAdmin1, this.<Asset, AssetId>exportEntity(tenantAdmin1, asset.getId())).getSavedEntity();
assertThat(importedAsset.getCustomerId()).isEqualTo(asset.getCustomerId());
}
@Test
public void testExportImportCustomer_betweenTenants() throws Exception {
Customer customer = createCustomer(tenantAdmin1.getTenantId(), "Customer of tenant 1");
EntityExportData<Customer> exportData = exportEntity(tenantAdmin1, customer.getId());
EntityImportResult<Customer> importResult = importEntity(tenantAdmin2, exportData);
checkImportedEntity(tenantId1, customer, tenantId2, importResult.getSavedEntity());
checkImportedCustomerData(customer, importResult.getSavedEntity());
}
@Test
public void testExportImportCustomer_sameTenant() throws Exception {
Customer customer = createCustomer(tenantAdmin1.getTenantId(), "Customer v1.0");
EntityExportData<Customer> exportData = exportEntity(tenantAdmin1, customer.getId());
EntityImportResult<Customer> importResult = importEntity(tenantAdmin1, exportData);
checkImportedEntity(tenantId1, customer, tenantId1, importResult.getSavedEntity());
checkImportedCustomerData(customer, importResult.getSavedEntity());
}
@Test
public void testExportImportDeviceWithProfile_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, null, null, "Device profile of tenant 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device of tenant 1");
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId1, device.getId());
EntityExportData<DeviceProfile> profileExportData = exportEntity(tenantAdmin1, deviceProfile.getId());
EntityExportData<Device> deviceExportData = exportEntity(tenantAdmin1, device.getId());
DeviceCredentials exportedCredentials = ((DeviceExportData) deviceExportData).getCredentials();
exportedCredentials.setCredentialsId(credentials.getCredentialsId() + "a");
EntityImportResult<DeviceProfile> profileImportResult = importEntity(tenantAdmin2, profileExportData);
checkImportedEntity(tenantId1, deviceProfile, tenantId2, profileImportResult.getSavedEntity());
checkImportedDeviceProfileData(deviceProfile, profileImportResult.getSavedEntity());
EntityImportResult<Device> deviceImportResult = importEntity(tenantAdmin2, deviceExportData);
Device importedDevice = deviceImportResult.getSavedEntity();
checkImportedEntity(tenantId1, device, tenantId2, deviceImportResult.getSavedEntity());
checkImportedDeviceData(device, importedDevice);
assertThat(importedDevice.getDeviceProfileId()).isEqualTo(profileImportResult.getSavedEntity().getId());
DeviceCredentials importedCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId2, importedDevice.getId());
assertThat(importedCredentials.getId()).isNotEqualTo(credentials.getId());
assertThat(importedCredentials.getCredentialsId()).isEqualTo(exportedCredentials.getCredentialsId());
assertThat(importedCredentials.getCredentialsValue()).isEqualTo(credentials.getCredentialsValue());
assertThat(importedCredentials.getCredentialsType()).isEqualTo(credentials.getCredentialsType());
}
@Test
public void testExportImportDevice_sameTenant() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device v1.0");
device.setFirmwareId(firmware.getId());
device.setSoftwareId(software.getId());
device = deviceService.saveDevice(device);
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId1, device.getId());
EntityExportData<Device> deviceExportData = exportEntity(tenantAdmin1, device.getId());
EntityImportResult<Device> importResult = importEntity(tenantAdmin1, deviceExportData);
Device importedDevice = importResult.getSavedEntity();
checkImportedEntity(tenantId1, device, tenantId1, importResult.getSavedEntity());
assertThat(importedDevice.getDeviceProfileId()).isEqualTo(device.getDeviceProfileId());
assertThat(deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId1, device.getId())).isEqualTo(credentials);
assertThat(importedDevice.getFirmwareId()).isEqualTo(firmware.getId());
assertThat(importedDevice.getSoftwareId()).isEqualTo(software.getId());
}
@Test
public void testExportImportDashboard_betweenTenants() throws Exception {
Dashboard dashboard = createDashboard(tenantAdmin1.getTenantId(), null, "Dashboard of tenant 1");
EntityExportData<Dashboard> exportData = exportEntity(tenantAdmin1, dashboard.getId());
EntityImportResult<Dashboard> importResult = importEntity(tenantAdmin2, exportData);
checkImportedEntity(tenantId1, dashboard, tenantId2, importResult.getSavedEntity());
checkImportedDashboardData(dashboard, importResult.getSavedEntity());
}
@Test
public void testExportImportDashboard_sameTenant() throws Exception {
Dashboard dashboard = createDashboard(tenantAdmin1.getTenantId(), null, "Dashboard v1.0");
EntityExportData<Dashboard> exportData = exportEntity(tenantAdmin1, dashboard.getId());
EntityImportResult<Dashboard> importResult = importEntity(tenantAdmin1, exportData);
checkImportedEntity(tenantId1, dashboard, tenantId1, importResult.getSavedEntity());
checkImportedDashboardData(dashboard, importResult.getSavedEntity());
}
@Test
public void testExportImportDashboard_betweenTenants_withCustomer_updated() throws Exception {
Dashboard dashboard = createDashboard(tenantAdmin1.getTenantId(), null, "Dashboard of tenant 1");
EntityExportData<Dashboard> exportData = exportEntity(tenantAdmin1, dashboard.getId());
Dashboard importedDashboard = importEntity(tenantAdmin2, exportData).getSavedEntity();
checkImportedEntity(tenantId1, dashboard, tenantId2, importedDashboard);
Customer customer = createCustomer(tenantId1, "Customer 1");
EntityExportData<Customer> customerExportData = exportEntity(tenantAdmin1, customer.getId());
dashboardService.assignDashboardToCustomer(tenantId1, dashboard.getId(), customer.getId());
exportData = exportEntity(tenantAdmin1, dashboard.getId());
Customer importedCustomer = importEntity(tenantAdmin2, customerExportData).getSavedEntity();
importedDashboard = importEntity(tenantAdmin2, exportData).getSavedEntity();
assertThat(importedDashboard.getAssignedCustomers()).hasOnlyOneElementSatisfying(customerInfo -> {
assertThat(customerInfo.getCustomerId()).isEqualTo(importedCustomer.getId());
});
}
@Test
public void testExportImportDashboard_betweenTenants_withEntityAliases() throws Exception {
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "A");
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
Dashboard otherDashboard = createDashboard(tenantId1, null, "Dashboard 2");
DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing");
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa";
String entityAliases = "{\n" +
"\"" + aliasId + "\": {\n" +
"\"alias\": \"assets\",\n" +
"\"filter\": {\n" +
" \"entityList\": [\n" +
" \"" + asset1.getId() + "\",\n" +
" \"" + asset2.getId() + "\",\n" +
" \"" + tenantId1.getId() + "\",\n" +
" \"" + existingDeviceProfile.getId() + "\",\n" +
" \"" + unknownUuid + "\"\n" +
" ],\n" +
" \"id\":\"" + asset1.getId() + "\",\n" +
" \"resolveMultiple\": true\n" +
"},\n" +
"\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}";
String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b";
String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae";
String widgets = "{\n" +
" \"" + widgetId + "\": {\n" +
" \"config\": {\n" +
" \"actions\": {\n" +
" \"rowClick\": [\n" +
" {\n" +
" \"name\": \"go to dashboard\",\n" +
" \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" +
" \"id\": \"" + actionId + "\"\n" +
" }\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"row\": 0,\n" +
" \"col\": 0,\n" +
" \"id\": \"" + widgetId + "\"\n" +
" }\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("widgets", JacksonUtil.toJsonNode(widgets));
dashboardConfiguration.set("description", new TextNode("hallo"));
dashboard.setConfiguration(dashboardConfiguration);
dashboard = dashboardService.saveDashboard(dashboard);
EntityExportData<AssetProfile> profileExportData = exportEntity(tenantAdmin1, assetProfile.getId());
EntityExportData<Asset> asset1ExportData = exportEntity(tenantAdmin1, asset1.getId());
EntityExportData<Asset> asset2ExportData = exportEntity(tenantAdmin1, asset2.getId());
EntityExportData<Dashboard> dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId());
EntityExportData<Dashboard> otherDashboardExportData = exportEntity(tenantAdmin1, otherDashboard.getId());
AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity();
Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity();
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedOtherDashboard = importEntity(tenantAdmin2, otherDashboardExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next();
assertThat(entityAlias.getKey()).isEqualTo(aliasId);
assertThat(entityAlias.getValue().get("id").asText()).isEqualTo(aliasId);
List<String> aliasEntitiesIds = Streams.stream(entityAlias.getValue().get("filter").get("entityList").elements())
.map(JsonNode::asText).collect(Collectors.toList());
assertThat(aliasEntitiesIds).size().isEqualTo(5);
assertThat(aliasEntitiesIds).element(0).as("external asset 1 was replaced with imported one")
.isEqualTo(importedAsset1.getId().toString());
assertThat(aliasEntitiesIds).element(1).as("external asset 2 was replaced with imported one")
.isEqualTo(importedAsset2.getId().toString());
assertThat(aliasEntitiesIds).element(2).as("external tenant id was replaced with new tenant id")
.isEqualTo(tenantId2.toString());
assertThat(aliasEntitiesIds).element(3).as("existing device profile id was left as is")
.isEqualTo(existingDeviceProfile.getId().toString());
assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id")
.isEqualTo(tenantId2.toString());
assertThat(entityAlias.getValue().get("filter").get("id").asText()).as("external asset 1 was replaced with imported one")
.isEqualTo(importedAsset1.getId().toString());
ObjectNode widgetConfig = importedDashboard.getWidgetsConfig().get(0);
assertThat(widgetConfig.get("id").asText()).as("widget id is not replaced")
.isEqualTo(widgetId);
JsonNode actionConfig = widgetConfig.get("config").get("actions").get("rowClick").get(0);
assertThat(actionConfig.get("id").asText()).as("action id is not replaced")
.isEqualTo(actionId);
assertThat(actionConfig.get("targetDashboardId").asText()).as("dashboard id is replaced with imported one")
.isEqualTo(importedOtherDashboard.getId().toString());
}
@Test
public void testExportImportRuleChain_betweenTenants() throws Exception {
RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain of tenant 1");
RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId1, ruleChain.getId());
EntityExportData<RuleChain> exportData = exportEntity(tenantAdmin1, ruleChain.getId());
EntityImportResult<RuleChain> importResult = importEntity(tenantAdmin2, exportData);
RuleChain importedRuleChain = importResult.getSavedEntity();
RuleChainMetaData importedMetaData = ruleChainService.loadRuleChainMetaData(tenantId2, importedRuleChain.getId());
checkImportedEntity(tenantId1, ruleChain, tenantId2, importResult.getSavedEntity());
checkImportedRuleChainData(ruleChain, metaData, importedRuleChain, importedMetaData);
}
@Test
public void testExportImportRuleChain_sameTenant() throws Exception {
RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain v1.0");
RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId1, ruleChain.getId());
EntityExportData<RuleChain> exportData = exportEntity(tenantAdmin1, ruleChain.getId());
EntityImportResult<RuleChain> importResult = importEntity(tenantAdmin1, exportData);
RuleChain importedRuleChain = importResult.getSavedEntity();
RuleChainMetaData importedMetaData = ruleChainService.loadRuleChainMetaData(tenantId1, importedRuleChain.getId());
checkImportedEntity(tenantId1, ruleChain, tenantId1, importResult.getSavedEntity());
checkImportedRuleChainData(ruleChain, metaData, importedRuleChain, importedMetaData);
}
@Test
public void testImportRuleChain_ruleNodesConfigs() throws Exception {
Customer customer = createCustomer(tenantId1, "Customer 1");
RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain 1");
RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId1, ruleChain.getId());
List<RuleNode> nodes = new ArrayList<>(metaData.getNodes());
RuleNode generatorNode = new RuleNode();
generatorNode.setName("Generator");
generatorNode.setType(TbMsgGeneratorNode.class.getName());
TbMsgGeneratorNodeConfiguration generatorNodeConfig = new TbMsgGeneratorNodeConfiguration();
generatorNodeConfig.setOriginatorType(EntityType.ASSET_PROFILE);
generatorNodeConfig.setOriginatorId(customer.getId().toString());
generatorNodeConfig.setPeriodInSeconds(5);
generatorNodeConfig.setMsgCount(1);
generatorNodeConfig.setScriptLang(ScriptLanguage.JS);
UUID someUuid = UUID.randomUUID();
generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
"var metadata = { data: 40 };\n" +
"var msgType = \"POST_TELEMETRY_REQUEST\";\n" +
"var someUuid = \"" + someUuid + "\";\n" +
"return { msg: msg, metadata: metadata, msgType: msgType };");
generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig));
nodes.add(generatorNode);
metaData.setNodes(nodes);
ruleChainService.saveRuleChainMetaData(tenantId1, metaData, Function.identity());
EntityExportData<RuleChain> ruleChainExportData = exportEntity(tenantAdmin1, ruleChain.getId());
EntityExportData<Customer> customerExportData = exportEntity(tenantAdmin1, customer.getId());
Customer importedCustomer = importEntity(tenantAdmin2, customerExportData).getSavedEntity();
RuleChain importedRuleChain = importEntity(tenantAdmin2, ruleChainExportData).getSavedEntity();
RuleChainMetaData importedMetaData = ruleChainService.loadRuleChainMetaData(tenantId2, importedRuleChain.getId());
TbMsgGeneratorNodeConfiguration importedGeneratorNodeConfig = JacksonUtil.treeToValue(importedMetaData.getNodes().stream()
.filter(node -> node.getName().equals(generatorNode.getName()))
.findFirst().get().getConfiguration(), TbMsgGeneratorNodeConfiguration.class);
assertThat(importedGeneratorNodeConfig.getOriginatorId()).isEqualTo(importedCustomer.getId().toString());
assertThat(importedGeneratorNodeConfig.getJsScript()).contains("var someUuid = \"" + someUuid + "\";");
}
@Test
public void testExportImportWithInboundRelations_betweenTenants() throws Exception {
Asset asset = createAsset(tenantId1, null, null, "Asset 1");
Device device = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation = createRelation(asset.getId(), device.getId());
EntityExportData<Asset> assetExportData = exportEntity(tenantAdmin1, asset.getId());
EntityExportData<Device> deviceExportData = exportEntity(tenantAdmin1, device.getId(), EntityExportSettings.builder()
.exportRelations(true)
.exportCredentials(false)
.build());
assertThat(deviceExportData.getRelations()).size().isOne();
assertThat(deviceExportData.getRelations().get(0)).matches(entityRelation -> {
return entityRelation.getFrom().equals(asset.getId()) && entityRelation.getTo().equals(device.getId());
});
((Asset) assetExportData.getEntity()).setAssetProfileId(null);
((Device) deviceExportData.getEntity()).setDeviceProfileId(null);
Asset importedAsset = importEntity(tenantAdmin2, assetExportData).getSavedEntity();
Device importedDevice = importEntity(tenantAdmin2, deviceExportData, EntityImportSettings.builder()
.updateRelations(true)
.build()).getSavedEntity();
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
List<EntityRelation> importedRelations = relationService.findByTo(TenantId.SYS_TENANT_ID, importedDevice.getId(), RelationTypeGroup.COMMON);
assertThat(importedRelations).size().isOne();
assertThat(importedRelations.get(0)).satisfies(importedRelation -> {
assertThat(importedRelation.getFrom()).isEqualTo(importedAsset.getId());
assertThat(importedRelation.getType()).isEqualTo(relation.getType());
assertThat(importedRelation.getAdditionalInfo()).isEqualTo(relation.getAdditionalInfo());
});
}
@Test
public void testExportImportWithRelations_betweenTenants() throws Exception {
Asset asset = createAsset(tenantId1, null, null, "Asset 1");
Device device = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation = createRelation(asset.getId(), device.getId());
EntityExportData<Asset> assetExportData = exportEntity(tenantAdmin1, asset.getId());
EntityExportData<Device> deviceExportData = exportEntity(tenantAdmin1, device.getId(), EntityExportSettings.builder()
.exportRelations(true)
.exportCredentials(false)
.build());
assetExportData.getEntity().setAssetProfileId(null);
deviceExportData.getEntity().setDeviceProfileId(null);
Asset importedAsset = importEntity(tenantAdmin2, assetExportData).getSavedEntity();
Device importedDevice = importEntity(tenantAdmin2, deviceExportData, EntityImportSettings.builder()
.updateRelations(true)
.build()).getSavedEntity();
List<EntityRelation> importedRelations = relationService.findByTo(TenantId.SYS_TENANT_ID, importedDevice.getId(), RelationTypeGroup.COMMON);
assertThat(importedRelations).size().isOne();
assertThat(importedRelations.get(0)).satisfies(importedRelation -> {
assertThat(importedRelation.getFrom()).isEqualTo(importedAsset.getId());
assertThat(importedRelation.getType()).isEqualTo(relation.getType());
assertThat(importedRelation.getAdditionalInfo()).isEqualTo(relation.getAdditionalInfo());
});
}
@Test
public void testExportImportWithRelations_sameTenant() throws Exception {
Asset asset = createAsset(tenantId1, null, null, "Asset 1");
Device device1 = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation1 = createRelation(asset.getId(), device1.getId());
EntityExportData<Asset> assetExportData = exportEntity(tenantAdmin1, asset.getId(), EntityExportSettings.builder()
.exportRelations(true)
.build());
assertThat(assetExportData.getRelations()).size().isOne();
Device device2 = createDevice(tenantId1, null, null, "Device 2");
EntityRelation relation2 = createRelation(asset.getId(), device2.getId());
importEntity(tenantAdmin1, assetExportData, EntityImportSettings.builder()
.updateRelations(true)
.build());
List<EntityRelation> relations = relationService.findByFrom(TenantId.SYS_TENANT_ID, asset.getId(), RelationTypeGroup.COMMON);
assertThat(relations).contains(relation1);
assertThat(relations).doesNotContain(relation2);
}
@Test
public void textExportImportWithRelations_sameTenant_removeExisting() throws Exception {
Asset asset1 = createAsset(tenantId1, null, null, "Asset 1");
Device device = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation1 = createRelation(asset1.getId(), device.getId());
EntityExportData<Device> deviceExportData = exportEntity(tenantAdmin1, device.getId(), EntityExportSettings.builder()
.exportRelations(true)
.build());
assertThat(deviceExportData.getRelations()).size().isOne();
Asset asset2 = createAsset(tenantId1, null, null, "Asset 2");
EntityRelation relation2 = createRelation(asset2.getId(), device.getId());
importEntity(tenantAdmin1, deviceExportData, EntityImportSettings.builder()
.updateRelations(true)
.build());
List<EntityRelation> relations = relationService.findByTo(TenantId.SYS_TENANT_ID, device.getId(), RelationTypeGroup.COMMON);
assertThat(relations).contains(relation1);
assertThat(relations).doesNotContain(relation2);
}
@Test
public void testExportImportDefaultDeviceProfile_betweenTenants_findExisting() throws Exception {
DeviceProfile defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId1);
defaultDeviceProfile.setName("non-default-name");
deviceProfileService.saveDeviceProfile(defaultDeviceProfile);
EntityExportData<DeviceProfile> deviceProfileExportData = exportEntity(tenantAdmin1, defaultDeviceProfile.getId());
importEntity(tenantAdmin2, deviceProfileExportData, EntityImportSettings.builder()
.findExistingByName(false)
.build());
DeviceProfile importedDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId2);
assertThat(importedDeviceProfile.isDefault()).isTrue();
assertThat(importedDeviceProfile.getName()).isEqualTo(defaultDeviceProfile.getName());
checkImportedEntity(tenantId1, defaultDeviceProfile, tenantId2, importedDeviceProfile);
@Autowired
protected EntitiesExportImportService exportImportService;
@Autowired
protected DeviceService deviceService;
@Autowired
protected OtaPackageService otaPackageService;
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected AssetService assetService;
@Autowired
protected CustomerService customerService;
@Autowired
protected RuleChainService ruleChainService;
@Autowired
protected DashboardService dashboardService;
@Autowired
protected RelationService relationService;
@Autowired
protected TenantService tenantService;
@Autowired
protected EntityViewService entityViewService;
protected TenantId tenantId1;
protected User tenantAdmin1;
protected TenantId tenantId2;
protected User tenantAdmin2;
@Before
public void beforeEach() throws Exception {
loginSysAdmin();
Tenant tenant1 = new Tenant();
tenant1.setTitle("Tenant 1");
tenant1.setEmail("tenant1@thingsboard.org");
this.tenantId1 = tenantService.saveTenant(tenant1).getId();
User tenantAdmin1 = new User();
tenantAdmin1.setTenantId(tenantId1);
tenantAdmin1.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin1.setEmail("tenant1-admin@thingsboard.org");
this.tenantAdmin1 = createUser(tenantAdmin1, "12345678");
Tenant tenant2 = new Tenant();
tenant2.setTitle("Tenant 2");
tenant2.setEmail("tenant2@thingsboard.org");
this.tenantId2 = tenantService.saveTenant(tenant2).getId();
User tenantAdmin2 = new User();
tenantAdmin2.setTenantId(tenantId2);
tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin2.setEmail("tenant2-admin@thingsboard.org");
this.tenantAdmin2 = createUser(tenantAdmin2, "12345678");
}
@SuppressWarnings("rawTypes")
private static EntityExportData getAndClone(Map<EntityType, EntityExportData> map, EntityType entityType) {
return JacksonUtil.clone(map.get(entityType));
@After
public void afterEach() {
tenantService.deleteTenant(tenantId1);
tenantService.deleteTenant(tenantId2);
}
@SuppressWarnings({"rawTypes", "unchecked"})
@ -712,4 +340,255 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile);
}
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
Device device = new Device();
device.setTenantId(tenantId);
device.setCustomerId(customerId);
device.setName(name);
device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId);
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
device.setDeviceData(deviceData);
return deviceService.saveDevice(device);
}
protected OtaPackage createOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType type) {
OtaPackage otaPackage = new OtaPackage();
otaPackage.setTenantId(tenantId);
otaPackage.setDeviceProfileId(deviceProfileId);
otaPackage.setType(type);
otaPackage.setTitle("My " + type);
otaPackage.setVersion("v1.0");
otaPackage.setFileName("filename.txt");
otaPackage.setContentType("text/plain");
otaPackage.setChecksumAlgorithm(ChecksumAlgorithm.SHA256);
otaPackage.setChecksum("4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a");
otaPackage.setDataSize(1L);
otaPackage.setData(ByteBuffer.wrap(new byte[]{(int) 1}));
return otaPackageService.saveOtaPackage(otaPackage);
}
protected DeviceProfile createDeviceProfile(TenantId tenantId, RuleChainId defaultRuleChainId, DashboardId defaultDashboardId, String name) {
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);
deviceProfile.setName(name);
deviceProfile.setDescription("dscrptn");
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
deviceProfile.setDefaultRuleChainId(defaultRuleChainId);
deviceProfile.setDefaultDashboardId(defaultDashboardId);
DeviceProfileData profileData = new DeviceProfileData();
profileData.setConfiguration(new DefaultDeviceProfileConfiguration());
profileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration());
deviceProfile.setProfileData(profileData);
return deviceProfileService.saveDeviceProfile(deviceProfile);
}
protected AssetProfile createAssetProfile(TenantId tenantId, RuleChainId defaultRuleChainId, DashboardId defaultDashboardId, String name) {
AssetProfile assetProfile = new AssetProfile();
assetProfile.setTenantId(tenantId);
assetProfile.setName(name);
assetProfile.setDescription("dscrptn");
assetProfile.setDefaultRuleChainId(defaultRuleChainId);
assetProfile.setDefaultDashboardId(defaultDashboardId);
return assetProfileService.saveAssetProfile(assetProfile);
}
protected Asset createAsset(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, String name) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setCustomerId(customerId);
asset.setAssetProfileId(assetProfileId);
asset.setName(name);
asset.setLabel("lbl");
asset.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
return assetService.saveAsset(asset);
}
protected Customer createCustomer(TenantId tenantId, String name) {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle(name);
customer.setCountry("ua");
customer.setAddress("abb");
customer.setEmail("ccc@aa.org");
customer.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
return customerService.saveCustomer(customer);
}
protected Dashboard createDashboard(TenantId tenantId, CustomerId customerId, String name) {
Dashboard dashboard = new Dashboard();
dashboard.setTenantId(tenantId);
dashboard.setTitle(name);
dashboard.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
dashboard.setImage("abvregewrg");
dashboard.setMobileHide(true);
dashboard = dashboardService.saveDashboard(dashboard);
if (customerId != null) {
dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerId);
return dashboardService.findDashboardById(tenantId, dashboard.getId());
}
return dashboard;
}
protected Dashboard createDashboard(TenantId tenantId, CustomerId customerId, String name, AssetId assetForEntityAlias) {
Dashboard dashboard = createDashboard(tenantId, customerId, name);
String entityAliases = "{\n" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" +
"\t\t\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" +
"\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" +
"\t\t\t],\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" +
"\t\t\t\"type\": \"entityList\"\n" +
"\t\t},\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" +
"\t}\n" +
"}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
dashboardConfiguration.set("description", new TextNode("hallo"));
dashboard.setConfiguration(dashboardConfiguration);
return dashboardService.saveDashboard(dashboard);
}
protected RuleChain createRuleChain(TenantId tenantId, String name, EntityId originatorId) {
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName(name);
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Generator 1");
ruleNode1.setType(TbMsgGeneratorNode.class.getName());
ruleNode1.setDebugMode(true);
TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration();
configuration1.setOriginatorType(originatorId.getEntityType());
configuration1.setOriginatorId(originatorId.getId().toString());
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
protected RuleChain createRuleChain(TenantId tenantId, String name) {
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName(name);
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
protected EntityView createEntityView(TenantId tenantId, CustomerId customerId, EntityId entityId, String name) {
EntityView entityView = new EntityView();
entityView.setTenantId(tenantId);
entityView.setEntityId(entityId);
entityView.setCustomerId(customerId);
entityView.setName(name);
entityView.setType("A");
return entityViewService.saveEntityView(entityView);
}
protected EntityRelation createRelation(EntityId from, EntityId to) {
EntityRelation relation = new EntityRelation();
relation.setFrom(from);
relation.setTo(to);
relation.setType(EntityRelation.MANAGES_TYPE);
relation.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
relation.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(TenantId.SYS_TENANT_ID, relation);
return relation;
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId) throws Exception {
return exportEntity(user, entityId, EntityExportSettings.builder()
.exportCredentials(true)
.build());
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId, EntityExportSettings exportSettings) throws Exception {
return exportImportService.exportEntity(new SimpleEntitiesExportCtx(getSecurityUser(user), null, null, exportSettings), entityId);
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData) throws Exception {
return importEntity(user, exportData, EntityImportSettings.builder()
.saveCredentials(true)
.build());
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData, EntityImportSettings importSettings) throws Exception {
EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), getSecurityUser(user), null, importSettings);
ctx.setFinalImportAttempt(true);
exportData = JacksonUtil.treeToValue(JacksonUtil.valueToTree(exportData), EntityExportData.class);
EntityImportResult<E> importResult = exportImportService.importEntity(ctx, exportData);
exportImportService.saveReferencesAndRelations(ctx);
for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) {
throwingRunnable.run();
}
return importResult;
}
@SuppressWarnings("rawTypes")
private static EntityExportData getAndClone(Map<EntityType, EntityExportData> map, EntityType entityType) {
return JacksonUtil.clone(map.get(entityType));
}
protected SecurityUser getSecurityUser(User user) {
return new SecurityUser(user, true, new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()));
}
}

1005
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

File diff suppressed because it is too large

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java

@ -33,6 +33,7 @@ public class RepositorySettings implements Serializable {
private String defaultBranch;
private boolean readOnly;
private boolean showMergeCommits;
private boolean localOnly;
public RepositorySettings() {
}
@ -48,5 +49,7 @@ public class RepositorySettings implements Serializable {
this.defaultBranch = settings.getDefaultBranch();
this.readOnly = settings.isReadOnly();
this.showMergeCommits = settings.isShowMergeCommits();
this.localOnly = settings.isLocalOnly();
}
}

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.edge.rpc;
import io.grpc.HttpConnectProxiedSocketAddress;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
@ -43,6 +44,7 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
@ -67,6 +69,16 @@ public class EdgeGrpcClient implements EdgeRpcClient {
private String certResource;
@Value("${cloud.rpc.max_inbound_message_size:4194304}")
private int maxInboundMessageSize;
@Value("${cloud.rpc.proxy.enabled}")
private boolean proxyEnabled;
@Value("${cloud.rpc.proxy.host:}")
private String proxyHost;
@Value("${cloud.rpc.proxy.port:0}")
private int proxyPort;
@Value("${cloud.rpc.proxy.username:}")
private String proxyUsername;
@Value("${cloud.rpc.proxy.password:}")
private String proxyPassword;
@Getter
private int serverMaxInboundMessageSize;
@ -88,6 +100,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.keepAliveTime(keepAliveTimeSec, TimeUnit.SECONDS)
.keepAliveTimeout(keepAliveTimeoutSec, TimeUnit.SECONDS)
.keepAliveWithoutCalls(true);
if (sslEnabled) {
try {
SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient();
@ -102,6 +115,18 @@ public class EdgeGrpcClient implements EdgeRpcClient {
} else {
builder.usePlaintext();
}
if (proxyEnabled && StringUtils.isNotEmpty(proxyHost) && proxyPort > 0) {
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHost, proxyPort);
InetSocketAddress targetAddress = new InetSocketAddress(rpcHost, rpcPort);
builder.proxyDetector(socketAddress -> HttpConnectProxiedSocketAddress.newBuilder()
.setTargetAddress(targetAddress)
.setProxyAddress(proxyAddress)
.setUsername(proxyUsername)
.setPassword(proxyPassword)
.build());
}
channel = builder.build();
EdgeRpcServiceGrpc.EdgeRpcServiceStub stub = EdgeRpcServiceGrpc.newStub(channel);
log.info("[{}] Sending a connect request to the TB!", edgeKey);

4
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -918,7 +918,8 @@ public class ProtoUtils {
.setRepositoryUri(repositorySettings.getRepositoryUri())
.setAuthMethod(repositorySettings.getAuthMethod().name())
.setReadOnly(repositorySettings.isReadOnly())
.setShowMergeCommits(repositorySettings.isShowMergeCommits());
.setShowMergeCommits(repositorySettings.isShowMergeCommits())
.setLocalOnly(repositorySettings.isLocalOnly());
if (isNotNull(repositorySettings.getUsername())) {
builder.setUsername(repositorySettings.getUsername());
@ -947,6 +948,7 @@ public class ProtoUtils {
repositorySettings.setAuthMethod(RepositoryAuthMethod.valueOf(proto.getAuthMethod()));
repositorySettings.setReadOnly(proto.getReadOnly());
repositorySettings.setShowMergeCommits(proto.getShowMergeCommits());
repositorySettings.setLocalOnly(proto.getLocalOnly());
if (proto.hasUsername()) {
repositorySettings.setUsername(proto.getUsername());
}

1
common/proto/src/main/proto/queue.proto

@ -324,6 +324,7 @@ message RepositorySettingsProto {
optional string defaultBranch = 8;
bool readOnly = 9;
bool showMergeCommits = 10;
bool localOnly = 11;
}
/**

2
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java

@ -202,7 +202,7 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
try {
Futures.allAsList(futures).get(packProcessingTimeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.info("Timeout for processing the version control tasks.", e);
log.error("Timeout for processing the version control tasks.", e);
}
consumer.commit();
}

50
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.sync.vc;
import jakarta.annotation.PostConstruct;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
@ -36,7 +37,6 @@ import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
import org.thingsboard.server.service.sync.vc.GitRepository.Diff;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -81,13 +81,20 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
GitRepository repository = checkRepository(commit.getTenantId());
String branch = commit.getBranch();
try {
repository.fetch();
repository.createAndCheckoutOrphanBranch(commit.getWorkingBranch());
repository.resetAndClean();
if (repository.listRemoteBranches().contains(new BranchInfo(branch, false))) {
repository.merge(branch);
List<String> branches = repository.listBranches().stream().map(BranchInfo::getName).toList();
if (repository.getSettings().isLocalOnly()) {
if (branches.contains(commit.getBranch())) {
repository.checkoutBranch(commit.getBranch());
} else {
repository.createAndCheckoutOrphanBranch(commit.getBranch());
}
repository.resetAndClean();
} else {
repository.createAndCheckoutOrphanBranch(commit.getWorkingBranch());
repository.resetAndClean();
if (branches.contains(branch)) {
repository.merge(branch);
}
}
} catch (IOException | GitAPIException gitAPIException) {
//TODO: analyze and return meaningful exceptions that we can show to the client;
@ -185,7 +192,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
public List<BranchInfo> listBranches(TenantId tenantId) {
GitRepository repository = checkRepository(tenantId);
try {
return repository.listRemoteBranches();
return repository.listBranches();
} catch (GitAPIException gitAPIException) {
//TODO: analyze and return meaningful exceptions that we can show to the client;
throw new RuntimeException(gitAPIException);
@ -233,9 +240,9 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
@Override
public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
testRepository(tenantId, settings);
clearRepository(tenantId);
if (!settings.isLocalOnly()) {
clearRepository(tenantId);
}
cloneRepository(tenantId, settings);
}
@ -247,11 +254,10 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
@Override
public void clearRepository(TenantId tenantId) throws IOException {
GitRepository repository = repositories.get(tenantId);
GitRepository repository = repositories.remove(tenantId);
if (repository != null) {
log.debug("[{}] Clear tenant repository started.", tenantId);
FileUtils.deleteDirectory(new File(repository.getDirectory()));
repositories.remove(tenantId);
log.debug("[{}] Clear tenant repository completed.", tenantId);
}
}
@ -276,15 +282,23 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
private GitRepository cloneRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
log.debug("[{}] Init tenant repository started.", tenantId);
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString());
Path repositoryDirectory = Path.of(repositoriesFolder, settings.isLocalOnly() ? "local_" + settings.getRepositoryUri() : tenantId.getId().toString());
GitRepository repository;
if (Files.exists(repositoryDirectory)) {
FileUtils.forceDelete(repositoryDirectory.toFile());
repository = GitRepository.open(repositoryDirectory.toFile(), settings);
} else {
Files.createDirectories(repositoryDirectory);
if (settings.isLocalOnly()) {
repository = GitRepository.create(settings, repositoryDirectory.toFile());
} else {
repository = GitRepository.clone(settings, repositoryDirectory.toFile());
}
}
Files.createDirectories(repositoryDirectory);
GitRepository repository = GitRepository.clone(settings, repositoryDirectory.toFile());
repositories.put(tenantId, repository);
log.debug("[{}] Init tenant repository completed.", tenantId);
return repository;
}
}

60
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -21,13 +21,13 @@ import com.google.common.collect.Streams;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.GitCommand;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.api.LsRemoteCommand;
import org.eclipse.jgit.api.ResetCommand;
@ -87,6 +87,9 @@ import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.eclipse.jgit.api.ListBranchCommand.ListMode;
@Slf4j
public class GitRepository {
private final Git git;
@ -106,7 +109,16 @@ public class GitRepository {
this.directory = directory;
}
public static GitRepository create(RepositorySettings settings, File directory) throws GitAPIException {
log.debug("Executing create [{}]", directory);
Git git = Git.init()
.setDirectory(directory)
.call();
return new GitRepository(git, settings, null, directory.getAbsolutePath());
}
public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException {
log.debug("Executing clone [{}]", settings.getRepositoryUri());
CloneCommand cloneCommand = Git.cloneRepository()
.setURI(settings.getRepositoryUri())
.setDirectory(directory)
@ -118,12 +130,17 @@ public class GitRepository {
}
public static GitRepository open(File directory, RepositorySettings settings) throws IOException {
log.debug("Executing open [{}][{}]", settings.getRepositoryUri(), directory);
Git git = Git.open(directory);
AuthHandler authHandler = AuthHandler.createFor(settings, directory);
return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
}
public static void test(RepositorySettings settings, File directory) throws Exception {
if (settings.isLocalOnly()) {
return;
}
log.debug("Executing test [{}]", settings.getRepositoryUri());
AuthHandler authHandler = AuthHandler.createFor(settings, directory);
if (settings.isReadOnly()) {
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
@ -147,6 +164,10 @@ public class GitRepository {
}
public void fetch() throws GitAPIException {
if (settings.isLocalOnly()) {
return;
}
log.debug("Executing fetch [{}]", settings.getRepositoryUri());
FetchResult result = execute(git.fetch()
.setRemoveDeletedRefs(true));
Ref head = result.getAdvertisedRef(Constants.HEAD);
@ -156,12 +177,14 @@ public class GitRepository {
}
public void deleteLocalBranchIfExists(String branch) throws GitAPIException {
log.debug("Executing deleteLocalBranchIfExists [{}][{}]", settings.getRepositoryUri(), branch);
execute(git.branchDelete()
.setBranchNames(branch)
.setForce(true));
}
public void resetAndClean() throws GitAPIException {
log.debug("Executing resetAndClean [{}]", settings.getRepositoryUri());
execute(git.reset()
.setMode(ResetCommand.ResetType.HARD));
execute(git.clean()
@ -170,6 +193,7 @@ public class GitRepository {
}
public void merge(String branch) throws IOException, GitAPIException {
log.debug("Executing merge [{}][{}]", settings.getRepositoryUri(), branch);
ObjectId branchId = resolve("origin/" + branch);
if (branchId == null) {
throw new IllegalArgumentException("Branch not found");
@ -178,9 +202,10 @@ public class GitRepository {
.include(branchId));
}
public List<BranchInfo> listRemoteBranches() throws GitAPIException {
public List<BranchInfo> listBranches() throws GitAPIException {
log.debug("Executing listBranches [{}]", settings.getRepositoryUri());
return execute(git.branchList()
.setListMode(ListBranchCommand.ListMode.REMOTE)).stream()
.setListMode(settings.isLocalOnly() ? ListMode.ALL : ListMode.REMOTE)).stream()
.filter(ref -> !ref.getName().equals(Constants.HEAD))
.map(this::toBranchInfo)
.distinct().collect(Collectors.toList());
@ -191,6 +216,7 @@ public class GitRepository {
}
public PageData<Commit> listCommits(String branch, String path, PageLink pageLink) throws IOException, GitAPIException {
log.debug("Executing listCommits [{}][{}][{}]", settings.getRepositoryUri(), branch, path);
ObjectId branchId = resolve("origin/" + branch);
if (branchId == null) {
return new PageData<>();
@ -212,6 +238,7 @@ public class GitRepository {
}
public List<String> listFilesAtCommit(String commitId, String path) throws IOException {
log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, path);
List<String> files = new ArrayList<>();
RevCommit revCommit = resolveCommit(commitId);
try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) {
@ -229,6 +256,7 @@ public class GitRepository {
public String getFileContentAtCommit(String file, String commitId) throws IOException {
log.debug("Executing getFileContentAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, file);
RevCommit revCommit = resolveCommit(commitId);
try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), file, revCommit.getTree())) {
if (treeWalk == null) {
@ -249,18 +277,29 @@ public class GitRepository {
public void createAndCheckoutOrphanBranch(String name) throws GitAPIException {
log.debug("Executing createAndCheckoutOrphanBranch [{}][{}]", settings.getRepositoryUri(), name);
execute(git.checkout()
.setOrphan(true)
.setForced(true)
.setName(name));
}
public void checkoutBranch(String name) throws GitAPIException {
log.debug("Executing checkoutBranch [{}][{}]", settings.getRepositoryUri(), name);
git.checkout()
.setForced(true)
.setName(name)
.call();
}
public void add(String filesPattern) throws GitAPIException {
log.debug("Executing add [{}][{}]", settings.getRepositoryUri(), filesPattern);
execute(git.add().setUpdate(true).addFilepattern(filesPattern));
execute(git.add().addFilepattern(filesPattern));
}
public Status status() throws GitAPIException {
log.debug("Executing status [{}]", settings.getRepositoryUri());
org.eclipse.jgit.api.Status status = execute(git.status());
Set<String> modified = new HashSet<>();
modified.addAll(status.getModified());
@ -269,6 +308,7 @@ public class GitRepository {
}
public Commit commit(String message, String authorName, String authorEmail) throws GitAPIException {
log.debug("Executing commit [{}][{}]", settings.getRepositoryUri(), message);
RevCommit revCommit = execute(git.commit()
.setAuthor(authorName, authorEmail)
.setMessage(message));
@ -277,6 +317,10 @@ public class GitRepository {
public void push(String localBranch, String remoteBranch) throws GitAPIException {
if (settings.isLocalOnly()) {
return;
}
log.debug("Executing push [{}][{}]", settings.getRepositoryUri(), remoteBranch);
execute(git.push()
.setRefSpecs(new RefSpec(localBranch + ":" + remoteBranch)));
}
@ -355,6 +399,9 @@ public class GitRepository {
}
private ObjectId resolve(String rev) throws IOException {
if (settings.isLocalOnly()) {
rev = StringUtils.removeStart(rev, "origin/");
}
ObjectId result = git.getRepository().resolve(rev);
if (result == null) {
throw new IllegalArgumentException("Failed to parse git revision string: \"" + rev + "\"");
@ -363,8 +410,8 @@ public class GitRepository {
}
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException {
if (command instanceof TransportCommand) {
authHandler.configureCommand((TransportCommand) command);
if (command instanceof TransportCommand transportCommand && authHandler != null) {
authHandler.configureCommand(transportCommand);
}
return command.call();
}
@ -412,6 +459,9 @@ public class GitRepository {
private final SshdSessionFactory sshSessionFactory;
protected static AuthHandler createFor(RepositorySettings settings, File directory) {
if (settings.isLocalOnly()) {
return null;
}
CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null;
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {

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

File diff suppressed because one or more lines are too long

1
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -186,6 +186,7 @@
<div fxFlex fxLayout="row" style="overflow: hidden; align-items: center;">
<span class="ellipsis">{{attribute.value | tbJson}}</span>
<tb-copy-button
*ngIf="attribute.value !== ''"
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.value | tbJson"

3
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html

@ -54,7 +54,8 @@
formControlName="deviceProfileId"
[displayAllOnEmpty]="true"
(deviceProfileChanged)="deviceProfileChanged($event)"
[editProfileEnabled]="false">
[editProfileEnabled]="false"
[addNewProfile]="false">
</tb-device-profile-autocomplete>
<mat-form-field>
<mat-label translate>device.device-state</mat-label>

45
ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts

@ -15,7 +15,17 @@
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -27,7 +37,6 @@ import {
defaultRpcRequestSchema,
defaultRpcResponseSchema,
defaultTelemetrySchema,
DeviceProfileTransportConfiguration,
DeviceTransportType,
TransportPayloadType,
transportPayloadTypeTranslationMap,
@ -41,13 +50,20 @@ import { PowerMode } from '@home/components/profile/device/lwm2m/lwm2m-profile-c
selector: 'tb-coap-device-profile-transport-configuration',
templateUrl: './coap-device-profile-transport-configuration.component.html',
styleUrls: ['./coap-device-profile-transport-configuration.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CoapDeviceProfileTransportConfigurationComponent),
multi: true
}
]
})
export class CoapDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy {
export class CoapDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator {
coapTransportDeviceTypes = Object.values(CoapTransportDeviceType);
coapTransportDeviceTypeTranslations = coapDeviceTypeTranslationMap;
@ -178,12 +194,15 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control
}
}
public validate(c: UntypedFormControl): ValidationErrors | null {
return (this.coapTransportConfigurationFormGroup.valid) ? null : {
valid: false,
};
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.coapTransportConfigurationFormGroup.valid) {
configuration = this.coapTransportConfigurationFormGroup.value;
configuration.type = DeviceTransportType.COAP;
}
const configuration = this.coapTransportConfigurationFormGroup.value;
configuration.type = DeviceTransportType.COAP;
this.propagateChange(configuration);
}
}

46
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts

@ -15,7 +15,17 @@
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -26,13 +36,20 @@ import { deepClone } from '@core/utils';
selector: 'tb-device-profile-transport-configuration',
templateUrl: './device-profile-transport-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceProfileTransportConfigurationComponent),
multi: true,
}
]
})
export class DeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit {
export class DeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator {
deviceTransportType = DeviceTransportType;
@ -98,11 +115,16 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.deviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.deviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
configuration.type = this.transportType;
}
const configuration = this.deviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
configuration.type = this.transportType;
this.propagateChange(configuration);
}
public validate(c: UntypedFormControl): ValidationErrors | null {
return (this.deviceProfileTransportConfigurationFormGroup.valid) ? null : {
configuration: {
valid: false,
},
};
}
}

6
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -296,11 +296,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
}
private updateModel = (): void => {
let configuration: Lwm2mProfileConfigModels = null;
if (this.lwm2mDeviceProfileFormGroup.valid) {
configuration = this.configurationValue;
}
this.propagateChange(configuration);
this.propagateChange(this.configurationValue);
}
private updateObserveAttrTelemetryObjectFormGroup = (objectsList: ObjectLwM2M[]): void => {

14
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts

@ -31,7 +31,7 @@ import { distinctUntilChanged, filter, mergeMap, share, tap } from 'rxjs/operato
import { ObjectLwM2M, PAGE_SIZE_LIMIT } from './lwm2m-profile-config.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { Direction } from '@shared/models/page/sort-order';
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { isDefined, isDefinedAndNotNull, isObject, isString } from '@core/utils';
import { PageLink } from '@shared/models/page/page-link';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
@ -90,12 +90,14 @@ export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, V
objectsList: [this.objectsList],
objectLwm2m: ['']
});
this.lwm2mListFormGroup.valueChanges.subscribe((value) => {
let formValue = null;
if (this.lwm2mListFormGroup.valid) {
formValue = value.objectsList;
this.lwm2mListFormGroup.get('objectsList').valueChanges.subscribe((value) => {
if (!value.length || (value.length && isObject(value[0]))) {
let formValue = null;
if (this.lwm2mListFormGroup.valid) {
formValue = value;
}
this.propagateChange(formValue);
}
this.propagateChange(formValue);
});
}

38
ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts

@ -17,10 +17,13 @@
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
@ -32,7 +35,6 @@ import {
defaultRpcRequestSchema,
defaultRpcResponseSchema,
defaultTelemetrySchema,
DeviceProfileTransportConfiguration,
DeviceTransportType,
MqttDeviceProfileTransportConfiguration,
TransportPayloadType,
@ -46,13 +48,20 @@ import { takeUntil } from 'rxjs/operators';
selector: 'tb-mqtt-device-profile-transport-configuration',
templateUrl: './mqtt-device-profile-transport-configuration.component.html',
styleUrls: ['./mqtt-device-profile-transport-configuration.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent),
multi: true
}
]
})
export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy {
export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator {
transportPayloadTypes = Object.keys(TransportPayloadType);
@ -172,12 +181,15 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
}
}
public validate(c: UntypedFormControl): ValidationErrors | null {
return (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) ? null : {
valid: false,
};
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.getRawValue();
configuration.type = DeviceTransportType.MQTT;
}
const configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.getRawValue();
configuration.type = DeviceTransportType.MQTT;
this.propagateChange(configuration);
}

8
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts

@ -27,7 +27,6 @@ import {
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceProfileTransportConfiguration,
DeviceTransportType,
SnmpDeviceProfileTransportConfiguration
} from '@shared/models/device.models';
@ -125,11 +124,8 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit,
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
configuration.type = DeviceTransportType.SNMP;
}
const configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue();
configuration.type = DeviceTransportType.SNMP;
this.propagateChange(configuration);
}

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-row tb-form-row no-border same-padding top-same-padding"
[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">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
@ -61,7 +61,7 @@
</div>
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding"
*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">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<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="tb-flex no-gap">
<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
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -58,6 +58,22 @@
</mat-form-field>
</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="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap">
@ -68,7 +84,8 @@
</mat-form-field>
</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-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | 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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -33,6 +33,7 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from "rxjs/operators";
@Component({
selector: 'tb-add-connector-dialog',
@ -40,7 +41,7 @@ import { ResourcesService } from '@core/services/resources.service';
styleUrls: ['./add-connector-dialog.component.scss'],
providers: [],
})
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnDestroy {
export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy {
connectorForm: UntypedFormGroup;
@ -66,9 +67,15 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
logLevel: [GatewayLogLevel.INFO, []],
useDefaults: [true, []],
sendDataOnlyOnChange: [false, []],
class: ['', []],
key: ['auto', []],
});
}
ngOnInit(): void {
this.observeTypeChange();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
@ -118,4 +125,18 @@ export class AddConnectorDialogComponent extends DialogComponent<AddConnectorDia
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 {
max-height: 670px;
height: 670px;
}

1263
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 {
width: 50%;
width: 100%;
.command-container {
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-cell *matCellDef="let attribute">
<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>
</ng-container>
<ng-container matColumnDef="key">
@ -178,18 +178,36 @@
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<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
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="connectorForm.get('name').hasError('required') && connectorForm.get('name').touched"
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ?
'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">
warning
</mat-icon>
</mat-form-field>
</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-title" translate>gateway.logs-configuration</div>
<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';
import { MatDialog } from '@angular/material/dialog';
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 { PageData } from '@shared/models/page/page-data';
@ -148,6 +148,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private destroy$ = new Subject<void>();
private subscription: IWidgetSubscription;
private attributeUpdateSubject = new Subject<AttributeData>();
constructor(protected store: Store<AppState>,
private fb: FormBuilder,
@ -180,6 +181,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
basicConfig: this.fb.group({})
});
this.connectorForm.disable();
this.observeAttributeChange();
}
get portErrorTooltip(): string {
@ -238,12 +241,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
if (sortHeaderId === 'syncStatus') {
return this.isConnectorSynced(data) ? 1 : 0;
} else if (sortHeaderId === 'enabled') {
return this.activeConnectors.includes(data.key) ? 1 : 0;
switch (sortHeaderId) {
case 'syncStatus':
return this.isConnectorSynced(data) ? 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) {
@ -524,31 +538,12 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
enableConnector(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;
onEnableConnector(attribute: AttributeData): void {
attribute.value.ts = new Date().getTime();
const tasks = [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])];
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);
});
this.updateActiveConnectorKeys(attribute.key);
this.attributeUpdateSubject.next(attribute);
}
getErrorsCount(attribute: AttributeData): string {
@ -599,6 +594,55 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
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 {
const exceptionData = this.utils.parseException(e);
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>
</nav>
<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()"
matSortDisableClear>
<ng-container matColumnDef="ts">
@ -31,7 +31,7 @@
</mat-cell>
</ng-container>
<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">
<span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</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() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
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>
<mat-expansion-panel hideToggle *ngFor="let template of rpcTemplates">
<mat-expansion-panel-header>
<mat-panel-title>
{{template.name}}
<mat-panel-title class="template-name">
<span matTooltip="{{template.name}}" matTooltipPosition="above">{{template.name}}</span>
</mat-panel-title>
<mat-panel-description>
<button mat-icon-button matTooltip="Delete" (click)="deleteTemplate($event, template)">
@ -47,7 +47,10 @@
<div *ngIf="!isObject(config.value) else RPCObjectRow"
[ngClass]="{'boolean-true': config.value === true,
'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-container
*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;
align-items: center;
margin-right: 0;
flex: 0;
& > mat-icon {
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 {
ConnectorType,
RPCTemplate
RPCTemplate,
SNMPMethodsTranslations
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { EntityType } from '@shared/models/entity-type.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 isObject = (value: any) => isLiteralObject(value);
public readonly SNMPMethodsTranslations = SNMPMethodsTranslations;
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 }}
</button>
</fieldset>
<fieldset class="fields border" fxLayout="column" fxLayoutGap="10px" formArrayName="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>
<tb-rest-connector-security [formControl]="commandForm.get('security')"></tb-rest-connector-security>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.REQUEST">
<mat-form-field>
@ -478,9 +447,6 @@
</button>
</fieldset>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA_ASYNCIO">
<ng-container *ngTemplateOutlet="OPCUAForm"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.OPCUA" #OPCUAForm>
<mat-form-field >
<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.
///
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
ControlValueAccessor,
FormArray,
@ -53,6 +53,8 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs";
@Component({
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()
connectorType: ConnectorType;
@ -105,6 +107,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
private propagateChange = (v: any) => {
}
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private dialog: MatDialog,) {
@ -114,17 +117,12 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
this.commandForm = this.connectorParamsFormGroupByType(this.connectorType);
this.commandForm.valueChanges.subscribe(value => {
const httpHeaders = {};
const security = {};
switch (this.connectorType) {
case ConnectorType.REST:
value.httpHeaders.forEach(data => {
httpHeaders[data.headerName] = data.value;
})
value.httpHeaders = httpHeaders;
value.security.forEach(data => {
security[data.securityName] = data.value;
})
value.security = security;
break;
case ConnectorType.REQUEST:
value.httpHeaders.forEach(data => {
@ -138,6 +136,12 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
}
});
this.isMQTTWithResponse = this.fb.control(false);
this.observeMQTTWithResponse();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
connectorParamsFormGroupByType(type: ConnectorType): FormGroup {
@ -148,7 +152,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
formGroup = this.fb.group({
methodFilter: [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)]],
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)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
httpHeaders: this.fb.array([]),
security: this.fb.array([])
security: [{}, [Validators.required]]
})
break;
case ConnectorType.REQUEST:
@ -269,7 +273,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
})
break;
case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
arguments: this.fb.array([]),
@ -312,22 +315,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
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) {
return (this.commandForm.get(path) as FormArray).controls as FormControl[];
}
@ -402,18 +389,12 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
break;
case ConnectorType.REST:
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 => {
this.addHTTPHeader({headerName: httpHeader[0], value: httpHeader[1] as string})
})
delete value.httpHeaders;
break;
case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
this.clearFromArrayByName("arguments");
value.arguments.forEach(value => {
this.addOCPUAArguments(value)
@ -424,4 +405,14 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, ControlValue
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.CAN:
case ConnectorType.OPCUA:
case ConnectorType.OPCUA_ASYNCIO:
return params.method;
case ConnectorType.BLE:
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',
GRPC = 'grpc',
OPCUA = 'opcua',
OPCUA_ASYNCIO = 'opcua_asyncio',
BLE = 'ble',
REQUEST = 'request',
CAN = 'can',
@ -145,7 +144,6 @@ export const GatewayConnectorDefaultTypesTranslatesMap = new Map<ConnectorType,
[ConnectorType.MODBUS, 'MODBUS'],
[ConnectorType.GRPC, 'GRPC'],
[ConnectorType.OPCUA, 'OPCUA'],
[ConnectorType.OPCUA_ASYNCIO, 'OPCUA ASYNCIO'],
[ConnectorType.BLE, 'BLE'],
[ConnectorType.REQUEST, 'REQUEST'],
[ConnectorType.CAN, 'CAN'],
@ -269,7 +267,7 @@ export const SNMPMethodsTranslations = new Map<SNMPMethods, string>([
[SNMPMethods.TABLE, 'gateway.rpc.table'],
[SNMPMethods.MULTIGET, 'gateway.rpc.multi-get'],
[SNMPMethods.GETNEXT, 'gateway.rpc.get-next'],
[SNMPMethods.BULKGET, 'gateway.rpc.bul-kget'],
[SNMPMethods.BULKGET, 'gateway.rpc.bulk-get'],
[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 = [
{ name: 3.1, value: 3 },
{ name: 3.11, value: 4 },

18
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -102,6 +102,9 @@ import { RadarChartWidgetComponent } from '@home/components/widget/lib/chart/rad
import { MobileAppQrcodeWidgetComponent } from '@home/components/widget/lib/mobile-app-qrcode-widget.component';
import { LabelCardWidgetComponent } from '@home/components/widget/lib/cards/label-card-widget.component';
import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards/label-value-card-widget.component';
import {
RestConnectorSecurityComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component';
@NgModule({
declarations:
@ -172,13 +175,14 @@ import { LabelValueCardWidgetComponent } from '@home/components/widget/lib/cards
LabelCardWidgetComponent,
LabelValueCardWidgetComponent
],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule
],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent
],
exports: [
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,

1
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -60,7 +60,6 @@
<tb-device-profile-autocomplete
required
addNewProfile
showCreateNewButton
selectDefaultProfile
formControlName="deviceProfileId"
(deviceProfileChanged)="deviceProfileChanged($event)">

2
ui-ngx/src/app/modules/home/models/datasource/attribute-datasource.ts

@ -89,7 +89,7 @@ export class AttributeDatasource implements DataSource<AttributeData> {
pageLink: PageLink): Observable<PageData<AttributeData>> {
return this.getAllAttributes(entityId, attributesScope).pipe(
map((data) => {
const filteredData = data.filter(attrData => attrData.lastUpdateTs !== 0 && attrData.value !== '');
const filteredData = data.filter(attrData => attrData.lastUpdateTs !== 0);
return pageLink.filterData(filteredData);
})
);

1
ui-ngx/src/app/modules/home/pages/asset/asset-table-header.component.html

@ -19,6 +19,7 @@
subscriptSizing="dynamic"
[ngModel]="entitiesTableConfig.componentsData.assetProfileId"
(ngModelChange)="assetProfileChanged($event)"
[addNewProfile]="false"
[displayAllOnEmpty]="true"
[editProfileEnabled]="false">
</tb-asset-profile-autocomplete>

11
ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html

@ -45,9 +45,16 @@
</div>
</mat-option>
<mat-option *ngIf="!(filteredWidgetTypes | async)?.length" [value]="null">
<span>
<div (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span>{{ 'widget.no-widgets-text' | translate }}</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }}
</span>
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
<mat-error>

4
ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts

@ -211,4 +211,8 @@ export class WidgetTypeAutocompleteComponent implements ControlValueAccessor, On
}, 0);
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
}

11
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.html

@ -57,9 +57,16 @@
<span [innerHTML]="dashboard.title | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredDashboards | async)?.length" [value]="null">
<span>
<div (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span>{{ 'dashboard.no-dashboards-text' | translate }}</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('dashboard.no-dashboards-matching', {entity: searchText}) | async }}
</span>
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!inlineField">

3
ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts

@ -276,4 +276,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI
}, 0);
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
}

13
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html

@ -40,9 +40,16 @@
<span [innerHTML]="entity.name | highlight:searchText:true:'ig'"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null">
<span>
{{ translate.get(noEntitiesMatchingText, {entity: searchText}) | async }}
</span>
<div (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span>{{ notFoundEntities | translate }}</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get(noEntitiesMatchingText, {entity: searchText}) | async }}
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required')">

17
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -64,6 +64,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
entityText: string;
noEntitiesMatchingText: string;
notFoundEntities = 'entity.no-entities-text';
entityRequiredText: string;
@ -201,63 +202,75 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
this.entityText = 'asset.asset';
this.noEntitiesMatchingText = 'asset.no-assets-matching';
this.entityRequiredText = 'asset.asset-required';
this.notFoundEntities = 'asset.no-assets-text';
break;
case EntityType.DEVICE:
this.entityText = 'device.device';
this.noEntitiesMatchingText = 'device.no-devices-matching';
this.entityRequiredText = 'device.device-required';
this.notFoundEntities = 'device.no-devices-text';
break;
case EntityType.EDGE:
this.entityText = 'edge.edge';
this.noEntitiesMatchingText = 'edge.no-edges-matching';
this.entityRequiredText = 'edge.edge-required';
this.notFoundEntities = 'edge.no-edges-text';
break;
case EntityType.ENTITY_VIEW:
this.entityText = 'entity-view.entity-view';
this.noEntitiesMatchingText = 'entity-view.no-entity-views-matching';
this.entityRequiredText = 'entity-view.entity-view-required';
this.notFoundEntities = 'entity-view.no-entity-views-text';
break;
case EntityType.RULE_CHAIN:
this.entityText = 'rulechain.rulechain';
this.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
this.entityRequiredText = 'rulechain.rulechain-required';
this.notFoundEntities = 'rulechain.no-rulechains-text';
break;
case EntityType.TENANT:
case AliasEntityType.CURRENT_TENANT:
this.entityText = 'tenant.tenant';
this.noEntitiesMatchingText = 'tenant.no-tenants-matching';
this.entityRequiredText = 'tenant.tenant-required';
this.notFoundEntities = 'tenant.no-tenants-text';
break;
case EntityType.CUSTOMER:
this.entityText = 'customer.customer';
this.noEntitiesMatchingText = 'customer.no-customers-matching';
this.entityRequiredText = 'customer.customer-required';
this.notFoundEntities = 'customer.no-customers-text';
break;
case EntityType.USER:
case AliasEntityType.CURRENT_USER:
this.entityText = 'user.user';
this.noEntitiesMatchingText = 'user.no-users-matching';
this.entityRequiredText = 'user.user-required';
this.notFoundEntities = 'user.no-users-text';
break;
case EntityType.DASHBOARD:
this.entityText = 'dashboard.dashboard';
this.noEntitiesMatchingText = 'dashboard.no-dashboards-matching';
this.entityRequiredText = 'dashboard.dashboard-required';
this.notFoundEntities = 'dashboard.no-dashboards-text';
break;
case EntityType.ALARM:
this.entityText = 'alarm.alarm';
this.noEntitiesMatchingText = 'alarm.no-alarms-matching';
this.entityRequiredText = 'alarm.alarm-required';
this.notFoundEntities = 'alarm.no-alarms-prompt';
break;
case EntityType.QUEUE_STATS:
this.entityText = 'queue-statistics.queue-statistics';
this.noEntitiesMatchingText = 'queue-statistics.no-queue-statistics-matching';
this.entityRequiredText = 'queue-statistics.queue-statistics-required';
this.notFoundEntities = 'queue-statistics.no-queue-statistics-text';
break;
case AliasEntityType.CURRENT_CUSTOMER:
this.entityText = 'customer.default-customer';
this.noEntitiesMatchingText = 'customer.no-customers-matching';
this.entityRequiredText = 'customer.default-customer-required';
this.notFoundEntities = 'customer.no-customers-text';
break;
case AliasEntityType.CURRENT_USER_OWNER:
const authUser = getCurrentAuthUser(this.store);
@ -375,6 +388,10 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
));
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
clear() {
this.selectEntityFormGroup.get('entity').patchValue('', {emitEvent: true});
setTimeout(() => {

11
ui-ngx/src/app/shared/components/entity/entity-list.component.html

@ -42,9 +42,16 @@
<span [innerHTML]="entity.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null">
<span>
<div (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span>{{ 'entity.no-entities-text' | translate }}</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }}
</span>
</span>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
<mat-hint *ngIf="hint">

4
ui-ngx/src/app/shared/components/entity/entity-list.component.ts

@ -257,4 +257,8 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, AfterV
}, 0);
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
}

21
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2177,6 +2177,7 @@
"add-entity-type": "Add entity type",
"enter-entity-type": "Enter entity type",
"no-entities-matching": "No entities matching '{{entity}}' were found.",
"no-entities-text": "No entities found",
"no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
"name-starts-with": "Name expression",
"help-text": "Use '%' according to need: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'.",
@ -2805,8 +2806,8 @@
"copy-username": "Copy username",
"copy-password": "Copy password",
"copy-client-id": "Copy client ID",
"connector-created": "Connector created.",
"connector-updated": "Connector updated.",
"connector-created": "Connector created",
"connector-updated": "Connector updated",
"rpc-command-save-template": "Save Template",
"rpc-command-send": "Send",
"rpc-command-result": "Response",
@ -3237,18 +3238,18 @@
"password": "MQTT password for the gateway form platform server",
"ca-cert": "Path to CA certificate file",
"date-form": "Date format in log message",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
"data-folder": "Path to the folder that will contain data (Relative or Absolute)",
"log-format": "Log message format",
"remote-log": "Enables remote logging and logs reading from the gateway",
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the platform",
"max-file-count": "Maximum count of file that will be created",
"max-read-count": "Count of messages to get from storage and send to platform",
"max-file-count": "Maximum number of files that will be created",
"max-read-count": "Number of messages to retrieve from the storage and send to platform",
"max-records": "Maximum count of records that will be stored in one file",
"read-record-count": "Count of messages to get from storage and send to platform",
"max-records-count": "Maximum count of data in storage before send to platform",
"ttl-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data",
"read-record-count": "Number of messages to retrieve from the storage and send to platform",
"max-records-count": "Maximum number of data entries in storage before sending to platform",
"ttl-check-hour": "How often will the Gateway check data for obsolescence",
"ttl-messages-day": "Maximum number of days that the storage will retain data",
"commands": "Commands for collecting additional statistic",
"attribute": "Statistic telemetry key",
"timeout": "Timeout for command executing",
@ -6080,8 +6081,8 @@
"event-key-contains": "Event key contains...",
"show-connector": "Show for the connector",
"connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"level": "Level",
"created-time": "Created time"
},
"gauge": {

1
ui-ngx/src/assets/locale/locale.constant-lt_LT.json

@ -6785,7 +6785,6 @@
"event-key-contains": "Event key contains...",
"show-connector": "Show for the connector",
"connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"created-time": "Created time"
},

1
ui-ngx/src/assets/locale/locale.constant-pl_PL.json

@ -6808,7 +6808,6 @@
"event-key-contains": "Klucz zdarzenia zawiera...",
"show-connector": "Show for the connector",
"connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"created-time": "Created time"
},

Loading…
Cancel
Save