Browse Source

Merge branch 'feature/swagger' into feature/swagger-device-profile-data

pull/5408/head
Dima Landiak 5 years ago
parent
commit
963efe6dd2
  1. 1
      application/pom.xml
  2. 11
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  3. 13
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  4. 13
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  5. 8
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  6. 2
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  7. 10
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  8. 114
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  9. 73
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  10. 127
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  11. 53
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  12. 80
      application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
  13. 14
      application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java
  14. 1
      application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
  15. 2
      application/src/main/resources/thingsboard.yml
  16. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
  17. 76
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  18. 5
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java
  19. 5
      common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java
  20. 78
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  21. 5
      common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java
  22. 26
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  23. 4
      common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java
  24. 6
      common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java
  25. 26
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
  26. 7
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java
  27. 5
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java
  28. 13
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java
  29. 8
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
  30. 29
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  32. 1
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java
  33. 4
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java
  34. 28
      common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java
  35. 12
      common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java
  36. 22
      dao/pom.xml
  37. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java
  38. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  39. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java
  40. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  41. 63
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  42. 5
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java
  43. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java
  44. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java
  45. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java
  46. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java
  47. 44
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  48. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java
  49. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java
  50. 30
      dao/src/test/java/org/thingsboard/server/dao/PostgreSqlDaoServiceTestSuite.java
  51. 64
      dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java
  52. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java
  53. 200
      dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java
  54. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java
  55. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java
  56. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java
  57. 33
      dao/src/test/java/org/thingsboard/server/dao/service/DaoPostgreSqlTest.java
  58. 8
      dao/src/test/java/org/thingsboard/server/dao/service/psql/EntityServicePostgreSqlTest.java
  59. 69
      dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepositoryTest.java
  60. 47
      dao/src/test/resources/psql-test.properties
  61. 2
      dao/src/test/resources/sql/system-test-psql.sql
  62. 11
      pom.xml
  63. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  64. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
  65. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  66. 68
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md
  67. 8
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/common_node_script_args.md
  68. 69
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/create_alarm_node_script_fn.md
  69. 69
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/filter_node_script_fn.md
  70. 118
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/generator_node_script_fn.md
  71. 37
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/log_node_script_fn.md
  72. 96
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/switch_node_script_fn.md
  73. 22
      rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/transformation_node_script_fn.md
  74. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  75. 1
      ui-ngx/angular.json
  76. 2
      ui-ngx/package.json
  77. 66
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  78. 29
      ui-ngx/src/app/core/services/help.service.ts
  79. 3
      ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html
  80. 3
      ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html
  81. 8
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html
  82. 6
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts
  83. 1
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  84. 2
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html
  85. 2
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  86. 2
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html
  87. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  88. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  89. 18
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  90. 4
      ui-ngx/src/app/shared/components/help-markdown.component.html
  91. 6
      ui-ngx/src/app/shared/components/help-markdown.component.scss
  92. 14
      ui-ngx/src/app/shared/components/help-markdown.component.ts
  93. 38
      ui-ngx/src/app/shared/components/help-popup.component.html
  94. 34
      ui-ngx/src/app/shared/components/help-popup.component.scss
  95. 62
      ui-ngx/src/app/shared/components/help-popup.component.ts
  96. 5
      ui-ngx/src/app/shared/components/json-form/json-form.component.ts
  97. 26
      ui-ngx/src/app/shared/components/markdown.component.html
  98. 194
      ui-ngx/src/app/shared/components/markdown.component.ts
  99. 30
      ui-ngx/src/app/shared/components/marked-options.service.ts
  100. 164
      ui-ngx/src/app/shared/components/popover.component.ts

1
application/pom.xml

@ -360,6 +360,7 @@
</systemPropertyVariables>
<excludes>
<exclude>**/sql/*Test.java</exclude>
<exclude>**/psql/*Test.java</exclude>
<exclude>**/nosql/*Test.java</exclude>
</excludes>
<includes>

11
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -133,7 +133,7 @@ public class AssetController extends BaseController {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
onAssetCreatedOrUpdated(savedAsset, asset.getId() != null);
onAssetCreatedOrUpdated(savedAsset, asset.getId() != null, getCurrentUser());
return savedAsset;
} catch (Exception e) {
@ -143,9 +143,9 @@ public class AssetController extends BaseController {
}
}
private void onAssetCreatedOrUpdated(Asset asset, boolean updated) {
private void onAssetCreatedOrUpdated(Asset asset, boolean updated, SecurityUser user) {
try {
logEntityAction(asset.getId(), asset,
logEntityAction(user, asset.getId(), asset,
asset.getCustomerId(),
updated ? ActionType.UPDATED : ActionType.ADDED, null);
} catch (ThingsboardException e) {
@ -656,8 +656,9 @@ public class AssetController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/asset/bulk_import")
public BulkImportResult<Asset> processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception {
return assetBulkImportService.processBulkImport(request, getCurrentUser(), importedAssetInfo -> {
onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated());
SecurityUser user = getCurrentUser();
return assetBulkImportService.processBulkImport(request, user, importedAssetInfo -> {
onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated(), user);
});
}

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

@ -169,6 +169,7 @@ public abstract class BaseController {
public static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
public static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
public static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
public static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
public static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
public static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
public static final String CUSTOMER_ID_PARAM_DESCRIPTION = "A string value representing the customer id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
@ -179,6 +180,8 @@ public abstract class BaseController {
public static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'";
public static final String RULE_CHAIN_ID_PARAM_DESCRIPTION = "A string value representing the rule chain id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority.";
protected static final String SYSTEM_AND_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority.";
protected static final String TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' authority.";
protected static final String TENANT_AND_USER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.";
@ -187,11 +190,15 @@ public abstract class BaseController {
protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile";
protected static final String ASSET_TYPE_DESCRIPTION = "Asset type";
protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'";
protected static final String RULE_CHAIN_TYPE_DESCRIPTION = "Rule chain type (CORE or EDGE)";
protected static final String ASSET_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the asset name.";
protected static final String DASHBOARD_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the dashboard title.";
protected static final String RPC_TEXT_SEARCH_DESCRIPTION = "Not implemented. Leave empty.";
protected static final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device name.";
protected static final String TENANT_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant name.";
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the customer title.";
protected static final String EDGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the edge name.";
@ -202,15 +209,21 @@ public abstract class BaseController {
protected static final String CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, city";
protected static final String RPC_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, expirationTime, request, response";
protected static final String DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deviceProfileName, label, customerTitle";
protected static final String TENANT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, state, city, address, address2, zip, phone, email";
protected static final String TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault";
protected static final String TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";
protected static final String TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, tenantProfileName, title, email, country, state, city, address, address2, zip, phone, email";
protected static final String DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, transportType, description, isDefault";
protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status";
protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, id";
protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root";
protected static final String AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, entityType, entityName, userName, actionType, actionStatus";
protected static final String SORT_ORDER_DESCRIPTION = "Sort order. ASC (ASCENDING) or DESC (DESCENDING)";
protected static final String SORT_ORDER_ALLOWABLE_VALUES = "ASC, DESC";
protected static final String RPC_STATUS_ALLOWABLE_VALUES = "QUEUED, SENT, DELIVERED, SUCCESSFUL, TIMEOUT, EXPIRED, FAILED";
protected static final String RULE_CHAIN_TYPES_ALLOWABLE_VALUES = "CORE, EDGE";
protected static final String TRANSPORT_TYPE_ALLOWABLE_VALUES = "DEFAULT, MQTT, COAP, LWM2M, SNMP";
protected static final String DEVICE_INFO_DESCRIPTION = "Device Info is an extension of the default Device object that contains information about the assigned customer name and device profile name. ";
protected static final String ASSET_INFO_DESCRIPTION = "Asset Info is an extension of the default Asset object that contains information about the assigned customer name. ";

13
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -163,7 +163,7 @@ public class DeviceController extends BaseController {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created);
onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created, getCurrentUser());
return savedDevice;
} catch (Exception e) {
@ -174,11 +174,11 @@ public class DeviceController extends BaseController {
}
private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated) {
private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated, SecurityUser user) {
tbClusterService.onDeviceUpdated(savedDevice, oldDevice);
try {
logEntityAction(savedDevice.getId(), savedDevice,
logEntityAction(user, savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
updated ? ActionType.UPDATED : ActionType.ADDED, null);
} catch (ThingsboardException e) {
@ -373,7 +373,7 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Get Tenant Devices (getTenantDevices)",
notes = "Returns a page of devices owned by tenant. " +
PAGE_DATA_PARAMETERS)
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@ -953,8 +953,9 @@ public class DeviceController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/device/bulk_import")
public BulkImportResult<Device> processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception {
return deviceBulkImportService.processBulkImport(request, getCurrentUser(), importedDeviceInfo -> {
onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated());
SecurityUser user = getCurrentUser();
return deviceBulkImportService.processBulkImport(request, user, importedDeviceInfo -> {
onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated(), user);
});
}

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

@ -163,7 +163,7 @@ public class EdgeController extends BaseController {
edge.getId(), edge);
Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true));
onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created);
onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created, getCurrentUser());
return savedEdge;
} catch (Exception e) {
@ -173,7 +173,7 @@ public class EdgeController extends BaseController {
}
}
private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated) throws IOException, ThingsboardException {
private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated, SecurityUser user) throws IOException, ThingsboardException {
if (!updated) {
ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edge.getId());
edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, edgeTemplateRootRuleChain.getId());
@ -183,7 +183,7 @@ public class EdgeController extends BaseController {
tbClusterService.broadcastEntityStateChangeEvent(edge.getTenantId(), edge.getId(),
updated ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED);
logEntityAction(edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null);
logEntityAction(user, edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null);
}
@ApiOperation(value = "Delete edge (deleteEdge)",
@ -707,7 +707,7 @@ public class EdgeController extends BaseController {
return edgeBulkImportService.processBulkImport(request, user, importedAssetInfo -> {
try {
onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated());
onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated(), user);
} catch (Exception e) {
throw new RuntimeException(e);
}

2
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -415,7 +415,7 @@ public class EntityQueryController extends BaseController {
" ]\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n YOu may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " +
"\n\n You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " +
"expression (for example, temperature > 'value of the tenant attribute with key 'temperatureThreshold'). " +
"It is possible to use 'dynamicValue' to define attribute of the tenant, customer or user that is performing the API call. " +
"See example below: \n\n" +

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

@ -91,9 +91,9 @@ public class RpcV2Controller extends AbstractRpcController {
"In case of persistent RPC, the result of this call is 'rpcId' UUID. In case of lightweight RPC, " +
"the result of this call is the response from device, or 504 Gateway Timeout if device is offline.";
private static final String ONE_WAY_RPC_REQUEST_DESCRIPTION = "Sends the one-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + ONE_WAY_RPC_RESULT;
private static final String ONE_WAY_RPC_REQUEST_DESCRIPTION = "Sends the one-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + ONE_WAY_RPC_RESULT + TENANT_AND_USER_AUTHORITY_PARAGRAPH;
private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT;
private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_AND_USER_AUTHORITY_PARAGRAPH;
@ApiOperation(value = "Send one-way RPC request", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION)
@ApiResponses(value = {
@ -131,7 +131,7 @@ public class RpcV2Controller extends AbstractRpcController {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}
@ApiOperation(value = "Get persistent RPC request", notes = "Get information about the status of the RPC call.")
@ApiOperation(value = "Get persistent RPC request", notes = "Get information about the status of the RPC call." + TENANT_AND_USER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.GET)
@ResponseBody
@ -147,7 +147,7 @@ public class RpcV2Controller extends AbstractRpcController {
}
}
@ApiOperation(value = "Get persistent RPC requests", notes = "Allows to query RPC calls for specific device using pagination.")
@ApiOperation(value = "Get persistent RPC requests", notes = "Allows to query RPC calls for specific device using pagination." + TENANT_AND_USER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persistent/device/{deviceId}", method = RequestMethod.GET)
@ResponseBody
@ -177,7 +177,7 @@ public class RpcV2Controller extends AbstractRpcController {
}
}
@ApiOperation(value = "Delete persistent RPC", notes = "Deletes the persistent RPC request.")
@ApiOperation(value = "Delete persistent RPC", notes = "Deletes the persistent RPC request." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.DELETE)
@ResponseBody

114
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -21,13 +21,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@ -95,6 +95,25 @@ public class RuleChainController extends BaseController {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static final int TIMEOUT = 20;
private static final String RULE_CHAIN_DESCRIPTION = "The rule chain object is lightweight and contains general information about the rule chain. " +
"List of rule nodes and their connection is stored in a separate 'metadata' object.";
private static final String RULE_CHAIN_METADATA_DESCRIPTION = "The metadata object contains information about the rule nodes and their connections.";
private static final String TEST_JS_FUNCTION = "Execute the JavaScript function and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ "{\n" +
" \"script\": \"Your JS Function as String\",\n" +
" \"scriptType\": \"One of: update, generate, filter, switch, json, string\",\n" +
" \"argNames\": [\"msg\", \"metadata\", \"type\"],\n" +
" \"msg\": \"{\\\"temperature\\\": 42}\", \n" +
" \"metadata\": {\n" +
" \"deviceName\": \"Device A\",\n" +
" \"deviceType\": \"Thermometer\"\n" +
" },\n" +
" \"msgType\": \"POST_TELEMETRY_REQUEST\"\n" +
"}"
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n Expected result JSON contains \"output\" and \"error\".";
@Autowired
private InstallScripts installScripts;
@ -110,10 +129,14 @@ public class RuleChainController extends BaseController {
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
private boolean debugPerTenantEnabled;
@ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
@ResponseBody
public RuleChain getRuleChainById(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
public RuleChain getRuleChainById(
@ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId);
try {
RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
@ -123,10 +146,14 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
@ResponseBody
public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
public RuleChainMetaData getRuleChainMetaData(
@ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId);
try {
RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
@ -137,11 +164,18 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Create Or Update Rule Chain (saveRuleChain)",
notes = "Create or update the Rule Chain. When creating Rule Chain, platform generates Rule Chain Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
"The newly created Rule Chain Id will be present in the response. " +
"Specify existing Rule Chain id to update the rule chain. " +
"Referencing non-existing rule chain Id will cause 'Not Found' error." +
"\n\n" + RULE_CHAIN_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
@ResponseBody
public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
public RuleChain saveRuleChain(
@ApiParam(value = "A JSON value representing the rule chain.")
@RequestBody RuleChain ruleChain) throws ThingsboardException {
try {
boolean created = ruleChain.getId() == null;
ruleChain.setTenantId(getCurrentUser().getTenantId());
@ -175,10 +209,15 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Create Default Rule Chain",
notes = "Create rule chain from template, based on the specified name in the request. " +
"Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST)
@ResponseBody
public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException {
public RuleChain saveRuleChain(
@ApiParam(value = "A JSON value representing the request.")
@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException {
try {
checkNotNull(request);
checkParameter(request.getName(), "name");
@ -198,10 +237,14 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)",
notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST)
@ResponseBody
public RuleChain setRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
public RuleChain setRootRuleChain(
@ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId);
try {
RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
@ -237,10 +280,14 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Update Rule Chain Metadata",
notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
@ResponseBody
public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
public RuleChainMetaData saveRuleChainMetaData(
@ApiParam(value = "A JSON value representing the rule chain metadata.")
@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
try {
TenantId tenantId = getTenantId();
if (debugPerTenantEnabled) {
@ -278,15 +325,24 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Get Rule Chains (getRuleChains)",
notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<RuleChain> getRuleChains(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = RULE_CHAIN_TYPE_DESCRIPTION, allowableValues = RULE_CHAIN_TYPES_ALLOWABLE_VALUES)
@RequestParam(value = "type", required = false) String typeStr,
@ApiParam(value = RULE_CHAIN_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
@ -301,10 +357,14 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Delete rule chain (deleteRuleChain)",
notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. Referencing rule chain that is used in the device profiles will cause an error.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
public void deleteRuleChain(
@ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId);
try {
RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
@ -347,10 +407,15 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)",
notes = "Gets the input message from the debug events for specified Rule Chain Id. " +
"Referencing non-existing rule chain Id will cause an error. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET)
@ResponseBody
public JsonNode getLatestRuleNodeDebugInput(@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException {
public JsonNode getLatestRuleNodeDebugInput(
@ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException {
checkParameter(RULE_NODE_ID, strRuleNodeId);
try {
RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId));
@ -373,10 +438,15 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Test JavaScript function",
notes = TEST_JS_FUNCTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
@ResponseBody
public JsonNode testScript(@RequestBody JsonNode inputParams) throws ThingsboardException {
public JsonNode testScript(
@ApiParam(value = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException {
try {
String script = inputParams.get("script").asText();
String scriptType = inputParams.get("scriptType").asText();
@ -436,10 +506,13 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET)
@ResponseBody
public RuleChainData exportRuleChains(@RequestParam("limit") int limit) throws ThingsboardException {
public RuleChainData exportRuleChains(
@ApiParam(value = "A limit of rule chains to export.", required = true)
@RequestParam("limit") int limit) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = new PageLink(limit);
@ -449,18 +522,25 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST)
@ResponseBody
public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException {
public List<RuleChainImportResult> importRuleChains(
@ApiParam(value = "A JSON value representing the rule chains.")
@RequestBody RuleChainData ruleChainData,
@ApiParam(value = "Enables overwrite for existing rule chains with the same name.")
@RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChainImportResult> importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, RuleChainType.CORE, overwrite);
if (!CollectionUtils.isEmpty(importResults)) {
for (RuleChainImportResult importResult : importResults) {
tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent());
List<RuleChainImportResult> importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite);
for (RuleChainImportResult importResult : importResults) {
if (importResult.getError() == null) {
tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(),
importResult.isUpdated() ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED);
}
}
return importResults;
} catch (Exception e) {
throw handleException(e);
}

73
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -47,21 +49,26 @@ import org.thingsboard.server.service.security.permission.Resource;
@Slf4j
public class TenantController extends BaseController {
private static final String TENANT_INFO_DESCRIPTION = "The Tenant Info object extends regular Tenant object and includes Tenant Profile name. ";
@Autowired
private InstallScripts installScripts;
@Autowired
private TenantService tenantService;
@ApiOperation(value = "Get Tenant (getTenantById)",
notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_AND_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.GET)
@ResponseBody
public Tenant getTenantById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException {
public Tenant getTenantById(
@ApiParam(value = TENANT_ID_PARAM_DESCRIPTION)
@PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException {
checkParameter(TENANT_ID, strTenantId);
try {
TenantId tenantId = new TenantId(toUUID(strTenantId));
Tenant tenant = checkTenantId(tenantId, Operation.READ);
if(!tenant.getAdditionalInfo().isNull()) {
if (!tenant.getAdditionalInfo().isNull()) {
processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD);
}
return tenant;
@ -70,10 +77,15 @@ public class TenantController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Info (getTenantInfoById)",
notes = "Fetch the Tenant Info object based on the provided Tenant Id. " +
TENANT_INFO_DESCRIPTION + SYSTEM_AND_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET)
@ResponseBody
public TenantInfo getTenantInfoById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException {
public TenantInfo getTenantInfoById(
@ApiParam(value = TENANT_ID_PARAM_DESCRIPTION)
@PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException {
checkParameter(TENANT_ID, strTenantId);
try {
TenantId tenantId = new TenantId(toUUID(strTenantId));
@ -83,10 +95,19 @@ public class TenantController extends BaseController {
}
}
@ApiOperation(value = "Create Or update Tenant (saveTenant)",
notes = "Create or update the Tenant. When creating tenant, platform generates Tenant Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
"Default Rule Chain and Device profile are also generated for the new tenants automatically. " +
"The newly created Tenant Id will be present in the response. " +
"Specify existing Tenant Id id to update the Tenant. " +
"Referencing non-existing Tenant Id will cause 'Not Found' error." +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant", method = RequestMethod.POST)
@ResponseBody
public Tenant saveTenant(@RequestBody Tenant tenant) throws ThingsboardException {
public Tenant saveTenant(
@ApiParam(value = "A JSON value representing the tenant.")
@RequestBody Tenant tenant) throws ThingsboardException {
try {
boolean newTenant = tenant.getId() == null;
@ -107,11 +128,15 @@ public class TenantController extends BaseController {
}
}
@ApiOperation(value = "Delete Tenant (deleteTenant)",
notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteTenant(@PathVariable("tenantId") String strTenantId) throws ThingsboardException {
checkParameter("tenantId", strTenantId);
public void deleteTenant(
@ApiParam(value = TENANT_ID_PARAM_DESCRIPTION)
@PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException {
checkParameter(TENANT_ID, strTenantId);
try {
TenantId tenantId = new TenantId(toUUID(strTenantId));
Tenant tenant = checkTenantId(tenantId, Operation.DELETE);
@ -124,14 +149,21 @@ public class TenantController extends BaseController {
}
}
@ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<Tenant> getTenants(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<Tenant> getTenants(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = TENANT_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(tenantService.findTenants(pageLink));
@ -140,14 +172,23 @@ public class TenantController extends BaseController {
}
}
@ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. "
+ TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<TenantInfo> getTenantInfos(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<TenantInfo> getTenantInfos(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = TENANT_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder
) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(tenantService.findTenantInfos(pageLink));

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

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
@ -44,10 +46,16 @@ import org.thingsboard.server.service.security.permission.Resource;
@Slf4j
public class TenantProfileController extends BaseController {
private static final String TENANT_PROFILE_INFO_DESCRIPTION = "Tenant Profile Info is a lightweight object that contains only id and name of the profile. ";
@ApiOperation(value = "Get Tenant Profile (getTenantProfileById)",
notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET)
@ResponseBody
public TenantProfile getTenantProfileById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
public TenantProfile getTenantProfileById(
@ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
checkParameter("tenantProfileId", strTenantProfileId);
try {
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
@ -57,10 +65,14 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Profile Info (getTenantProfileInfoById)",
notes = "Fetch the Tenant Profile Info object based on the provided Tenant Profile Id. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET)
@ResponseBody
public EntityInfo getTenantProfileInfoById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
public EntityInfo getTenantProfileInfoById(
@ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
checkParameter("tenantProfileId", strTenantProfileId);
try {
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
@ -70,6 +82,8 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)",
notes = "Fetch the default Tenant Profile Info object based. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET)
@ResponseBody
@ -81,10 +95,70 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Create Or update Tenant Profile (saveTenantProfile)",
notes = "Create or update the Tenant Profile. When creating tenant profile, platform generates Tenant Profile Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
"The newly created Tenant Profile Id will be present in the response. " +
"Specify existing Tenant Profile Id id to update the Tenant Profile. " +
"Referencing non-existing Tenant Profile Id will cause 'Not Found' error. " +
"\n\nUpdate of the tenant profile configuration will cause immediate recalculation of API limits for all affected Tenants. " +
"\n\nThe **'profileData'** object is the part of Tenant Profile that defines API limits and Rate limits. " +
"\n\nYou have an ability to define maximum number of devices ('maxDevice'), assets ('maxAssets') and other entities. " +
"You may also define maximum number of messages to be processed per month ('maxTransportMessages', 'maxREExecutions', etc). " +
"The '*RateLimit' defines the rate limits using simple syntax. For example, '1000:1,20000:60' means up to 1000 events per second but no more than 20000 event per minute. " +
"Let's review the example of tenant profile data below: " +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"id\": {\n" +
" \"entityType\": \"TENANT_PROFILE\",\n" +
" \"id\": \"0f2978a0-0d46-11eb-ab90-09ceaa526dd8\"\n" +
" },\n" +
" \"createdTime\": 1602588011818,\n" +
" \"name\": \"Default\",\n" +
" \"description\": \"Default tenant profile\",\n" +
" \"isolatedTbCore\": false,\n" +
" \"isolatedTbRuleEngine\": false,\n" +
" \"profileData\": {\n" +
" \"configuration\": {\n" +
" \"type\": \"DEFAULT\",\n" +
" \"maxDevices\": 0,\n" +
" \"maxAssets\": 0,\n" +
" \"maxCustomers\": 0,\n" +
" \"maxUsers\": 0,\n" +
" \"maxDashboards\": 0,\n" +
" \"maxRuleChains\": 0,\n" +
" \"maxResourcesInBytes\": 0,\n" +
" \"maxOtaPackagesInBytes\": 0,\n" +
" \"transportTenantMsgRateLimit\": \"1000:1,20000:60\",\n" +
" \"transportTenantTelemetryMsgRateLimit\": \"1000:1,20000:60\",\n" +
" \"transportTenantTelemetryDataPointsRateLimit\": \"1000:1,20000:60\",\n" +
" \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" +
" \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" +
" \"maxTransportMessages\": 10000000,\n" +
" \"maxTransportDataPoints\": 10000000,\n" +
" \"maxREExecutions\": 4000000,\n" +
" \"maxJSExecutions\": 5000000,\n" +
" \"maxDPStorageDays\": 0,\n" +
" \"maxRuleNodeExecutionsPerMessage\": 50,\n" +
" \"maxEmails\": 0,\n" +
" \"maxSms\": 0,\n" +
" \"maxCreatedAlarms\": 1000,\n" +
" \"defaultStorageTtlDays\": 0,\n" +
" \"alarmsTtlDays\": 0,\n" +
" \"rpcTtlDays\": 0,\n" +
" \"warnThreshold\": 0\n" +
" }\n" +
" },\n" +
" \"default\": true\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
@ResponseBody
public TenantProfile saveTenantProfile(@RequestBody TenantProfile tenantProfile) throws ThingsboardException {
public TenantProfile saveTenantProfile(
@ApiParam(value = "A JSON value representing the tenant profile.")
@RequestBody TenantProfile tenantProfile) throws ThingsboardException {
try {
boolean newTenantProfile = tenantProfile.getId() == null;
if (newTenantProfile) {
@ -105,10 +179,14 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)",
notes = "Deletes the tenant profile. Referencing non-existing tenant profile Id will cause an error. Referencing profile that is used by the tenants will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
public void deleteTenantProfile(
@ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
checkParameter("tenantProfileId", strTenantProfileId);
try {
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
@ -120,10 +198,14 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)",
notes = "Makes specified tenant profile to be default. Referencing non-existing tenant profile Id will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST)
@ResponseBody
public TenantProfile setDefaultTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
public TenantProfile setDefaultTenantProfile(
@ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
checkParameter("tenantProfileId", strTenantProfileId);
try {
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
@ -135,14 +217,21 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Profiles (getTenantProfiles)", notes = "Returns a page of tenant profiles registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<TenantProfile> getTenantProfiles(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<TenantProfile> getTenantProfiles(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink));
@ -151,14 +240,22 @@ public class TenantProfileController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. "
+ TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<EntityInfo> getTenantProfileInfos(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<EntityInfo> getTenantProfileInfos(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink));

53
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -63,6 +63,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@TbCoreComponent
@ -71,6 +73,8 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
protected final DeviceCredentialsService deviceCredentialsService;
protected final DeviceProfileService deviceProfileService;
private final Lock findOrCreateDeviceProfileLock = new ReentrantLock();
public DeviceBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
AccessControlService accessControlService, AccessValidator accessValidator,
EntityActionService entityActionService, TbClusterService clusterService,
@ -106,9 +110,13 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage());
}
DeviceProfile deviceProfile;
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) {
setUpLwM2mDeviceProfile(user.getTenantId(), device);
deviceProfile = setUpLwM2mDeviceProfile(user.getTenantId(), device);
} else {
deviceProfile = deviceProfileService.findOrCreateDeviceProfile(user.getTenantId(), device.getType());
}
device.setDeviceProfileId(deviceProfile.getId());
device = deviceService.saveDeviceWithCredentials(device, deviceCredentials);
@ -215,36 +223,43 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
credentials.setCredentialsValue(lwm2mCredentials.toString());
}
private void setUpLwM2mDeviceProfile(TenantId tenantId, Device device) {
private DeviceProfile setUpLwM2mDeviceProfile(TenantId tenantId, Device device) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByName(tenantId, device.getType());
if (deviceProfile != null) {
if (deviceProfile.getTransportType() != DeviceTransportType.LWM2M) {
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.getProfileData().setTransportConfiguration(new Lwm2mDeviceProfileTransportConfiguration());
deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
device.setDeviceProfileId(deviceProfile.getId());
}
} else {
deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setName(device.getType());
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
findOrCreateDeviceProfileLock.lock();
try {
deviceProfile = deviceProfileService.findDeviceProfileByName(tenantId, device.getType());
if (deviceProfile == null) {
deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);
deviceProfile.setType(DeviceProfileType.DEFAULT);
deviceProfile.setName(device.getType());
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
deviceProfileData.setConfiguration(configuration);
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfileData.setProvisionConfiguration(provisionConfiguration);
deviceProfile.setProfileData(deviceProfileData);
deviceProfileData.setConfiguration(configuration);
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfileData.setProvisionConfiguration(provisionConfiguration);
deviceProfile.setProfileData(deviceProfileData);
deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
device.setDeviceProfileId(deviceProfile.getId());
deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
}
} finally {
findOrCreateDeviceProfileLock.unlock();
}
}
return deviceProfile;
}
private void setValues(ObjectNode objectNode, Map<BulkImportColumnType, String> data, Collection<BulkImportColumnType> columns) {

80
application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java

@ -22,6 +22,9 @@ import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.TenantProfile;
@ -47,11 +50,16 @@ import org.thingsboard.server.utils.CsvUtils;
import org.thingsboard.server.utils.TypeCastUtil;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@ -67,39 +75,49 @@ public abstract class AbstractBulkImportService<E extends BaseData<? extends Ent
protected final EntityActionService entityActionService;
protected final TbClusterService clusterService;
public final BulkImportResult<E> processBulkImport(BulkImportRequest request, SecurityUser user, Consumer<ImportedEntityInfo<E>> onEntityImported) throws Exception {
BulkImportResult<E> result = new BulkImportResult<>();
private static ThreadPoolExecutor executor;
AtomicInteger i = new AtomicInteger(0);
if (request.getMapping().getHeader()) {
i.incrementAndGet();
@PostConstruct
private void initExecutor() {
if (executor == null) {
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(),
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(150_000),
ThingsBoardThreadFactory.forName("bulk-import"), new ThreadPoolExecutor.CallerRunsPolicy());
executor.allowCoreThreadTimeOut(true);
}
}
parseData(request).forEach(entityData -> {
i.incrementAndGet();
try {
ImportedEntityInfo<E> importedEntityInfo = saveEntity(request, entityData.getFields(), user);
onEntityImported.accept(importedEntityInfo);
public final BulkImportResult<E> processBulkImport(BulkImportRequest request, SecurityUser user, Consumer<ImportedEntityInfo<E>> onEntityImported) throws Exception {
List<EntityData> entitiesData = parseData(request);
E entity = importedEntityInfo.getEntity();
BulkImportResult<E> result = new BulkImportResult<>();
CountDownLatch completionLatch = new CountDownLatch(entitiesData.size());
saveKvs(user, entity, entityData.getKvs());
entitiesData.forEach(entityData -> DonAsynchron.submit(() -> {
ImportedEntityInfo<E> importedEntityInfo = saveEntity(request, entityData.getFields(), user);
E entity = importedEntityInfo.getEntity();
if (importedEntityInfo.getRelatedError() != null) {
throw new RuntimeException(importedEntityInfo.getRelatedError());
}
onEntityImported.accept(importedEntityInfo);
saveKvs(user, entity, entityData.getKvs());
if (importedEntityInfo.isUpdated()) {
result.setUpdated(result.getUpdated() + 1);
} else {
result.setCreated(result.getCreated() + 1);
}
} catch (Exception e) {
result.setErrors(result.getErrors() + 1);
result.getErrorsList().add(String.format("Line %d: %s", i.get(), e.getMessage()));
}
});
return importedEntityInfo;
},
importedEntityInfo -> {
if (importedEntityInfo.isUpdated()) {
result.getUpdated().incrementAndGet();
} else {
result.getCreated().incrementAndGet();
}
completionLatch.countDown();
},
throwable -> {
result.getErrors().incrementAndGet();
result.getErrorsList().add(String.format("Line %d: %s", entityData.getLineNumber(), ExceptionUtils.getRootCauseMessage(throwable)));
completionLatch.countDown();
},
executor));
completionLatch.await();
return result;
}
@ -186,8 +204,11 @@ public abstract class AbstractBulkImportService<E extends BaseData<? extends Ent
private List<EntityData> parseData(BulkImportRequest request) throws Exception {
List<List<String>> records = CsvUtils.parseCsv(request.getFile(), request.getMapping().getDelimiter());
AtomicInteger linesCounter = new AtomicInteger(0);
if (request.getMapping().getHeader()) {
records.remove(0);
linesCounter.incrementAndGet();
}
List<ColumnMapping> columnsMappings = request.getMapping().getColumns();
@ -205,15 +226,24 @@ public abstract class AbstractBulkImportService<E extends BaseData<? extends Ent
entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey()));
}
});
entityData.setLineNumber(linesCounter.incrementAndGet());
return entityData;
})
.collect(Collectors.toList());
}
@PreDestroy
private void shutdownExecutor() {
if (!executor.isTerminating()) {
executor.shutdown();
}
}
@Data
protected static class EntityData {
private final Map<BulkImportColumnType, String> fields = new LinkedHashMap<>();
private final Map<ColumnMapping, ParsedValue> kvs = new LinkedHashMap<>();
private int lineNumber;
}
@Data

14
application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java

@ -17,14 +17,14 @@ package org.thingsboard.server.service.importing;
import lombok.Data;
import java.util.LinkedList;
import java.util.List;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
@Data
public class BulkImportResult<E> {
private int created = 0;
private int updated = 0;
private int errors = 0;
private List<String> errorsList = new LinkedList<>();
private AtomicInteger created = new AtomicInteger();
private AtomicInteger updated = new AtomicInteger();
private AtomicInteger errors = new AtomicInteger();
private Collection<String> errorsList = new ConcurrentLinkedDeque<>();
}

1
application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java

@ -22,5 +22,4 @@ public class ImportedEntityInfo<E> {
private E entity;
private boolean isUpdated;
private E oldEntity;
private String relatedError;
}

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

@ -279,6 +279,8 @@ sql:
rpc:
enabled: "${SQL_TTL_RPC_ENABLED:true}"
checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
relations:
max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # //This value has to be reasonable small to prevent infinite recursion as early as possible
# Actor system parameters
actors:

3
common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java

@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainData;
@ -71,7 +70,7 @@ public interface RuleChainService {
RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException;
List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite);
List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite);
RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId);

76
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -18,6 +18,8 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
@ -27,7 +29,9 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId {
private static final long serialVersionUID = -1599722990298929275L;
@NoXss
@ApiModelProperty(position = 3, value = "Title of the customer", example = "Company A")
private String title;
@ApiModelProperty(position = 5, required = true, value = "JSON object with Tenant Id")
private TenantId tenantId;
public Customer() {
@ -51,7 +55,7 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId {
public void setTenantId(TenantId tenantId) {
this.tenantId = tenantId;
}
public String getTitle() {
return title;
}
@ -60,6 +64,75 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId {
this.title = title;
}
@ApiModelProperty(position = 1, value = "JSON object with the customer Id. " +
"Specify this field to update the customer. " +
"Referencing non-existing customer Id will cause error. " +
"Omit this field to create new customer." )
@Override
public CustomerId getId() {
return super.getId();
}
@ApiModelProperty(position = 2, value = "Timestamp of the customer creation, in milliseconds", example = "1609459200000", readOnly = true)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
@ApiModelProperty(position = 6, required = true, value = "Country", example = "US")
@Override
public String getCountry() {
return super.getCountry();
}
@ApiModelProperty(position = 7, required = true, value = "State", example = "NY")
@Override
public String getState() {
return super.getState();
}
@ApiModelProperty(position = 8, required = true, value = "City", example = "New York")
@Override
public String getCity() {
return super.getCity();
}
@ApiModelProperty(position = 9, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override
public String getAddress() {
return super.getAddress();
}
@ApiModelProperty(position = 10, required = true, value = "Address Line 2", example = "")
@Override
public String getAddress2() {
return super.getAddress2();
}
@ApiModelProperty(position = 11, required = true, value = "Zip code", example = "10004")
@Override
public String getZip() {
return super.getZip();
}
@ApiModelProperty(position = 12, required = true, value = "Phone number", example = "+1(415)777-7777")
@Override
public String getPhone() {
return super.getPhone();
}
@ApiModelProperty(position = 13, required = true, value = "Email", example = "example@company.com")
@Override
public String getEmail() {
return super.getEmail();
}
@ApiModelProperty(position = 14, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode")
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
@JsonIgnore
public boolean isPublic() {
if (getAdditionalInfo() != null && getAdditionalInfo().has("isPublic")) {
@ -76,6 +149,7 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId {
@Override
@JsonProperty(access = Access.READ_ONLY)
@ApiModelProperty(position = 4, value = "Name of the customer. Read-only, duplicated from title for backward compatibility", example = "Company A", readOnly = true)
public String getName() {
return title;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Value;
@ -31,9 +32,13 @@ import java.util.UUID;
@ToString(callSuper = true)
public class DeviceProfileInfo extends EntityInfo {
@ApiModelProperty(position = 3, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ")
private final String image;
@ApiModelProperty(position = 4, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to device details.")
private final DashboardId defaultDashboardId;
@ApiModelProperty(position = 5, value = "Type of the profile. Always 'DEFAULT' for now. Reserved for future use.")
private final DeviceProfileType type;
@ApiModelProperty(position = 6, value = "Type of the transport used to connect the device. Default transport supports HTTP, CoAP and MQTT.")
private final DeviceTransportType transportType;
@JsonCreator

5
common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -24,10 +26,13 @@ import org.thingsboard.server.common.data.id.HasId;
import java.util.UUID;
@ApiModel
@Data
public class EntityInfo implements HasId<EntityId>, HasName {
@ApiModelProperty(position = 1, value = "JSON object with the entity Id. ")
private final EntityId id;
@ApiModelProperty(position = 2, value = "Entity Name")
private final String name;
@JsonCreator

78
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -17,20 +17,28 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel
@EqualsAndHashCode(callSuper = true)
public class Tenant extends ContactBased<TenantId> implements HasTenantId {
private static final long serialVersionUID = 8057243243859922101L;
@NoXss
@ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A")
private String title;
@NoXss
@ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America")
private String region;
@ApiModelProperty(position = 6, required = true, value = "JSON object with Tenant Profile Id")
private TenantProfileId tenantProfileId;
public Tenant() {
@ -63,6 +71,7 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId {
}
@Override
@ApiModelProperty(position = 4, value = "Name of the tenant. Read-only, duplicated from title for backward compatibility", example = "Company A", readOnly = true)
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {
return title;
@ -89,6 +98,75 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId {
return getTitle();
}
@ApiModelProperty(position = 1, value = "JSON object with the tenant Id. " +
"Specify this field to update the tenant. " +
"Referencing non-existing tenant Id will cause error. " +
"Omit this field to create new tenant." )
@Override
public TenantId getId() {
return super.getId();
}
@ApiModelProperty(position = 2, value = "Timestamp of the tenant creation, in milliseconds", example = "1609459200000", readOnly = true)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
@ApiModelProperty(position = 7, required = true, value = "Country", example = "US")
@Override
public String getCountry() {
return super.getCountry();
}
@ApiModelProperty(position = 8, required = true, value = "State", example = "NY")
@Override
public String getState() {
return super.getState();
}
@ApiModelProperty(position = 9, required = true, value = "City", example = "New York")
@Override
public String getCity() {
return super.getCity();
}
@ApiModelProperty(position = 10, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override
public String getAddress() {
return super.getAddress();
}
@ApiModelProperty(position = 11, required = true, value = "Address Line 2", example = "")
@Override
public String getAddress2() {
return super.getAddress2();
}
@ApiModelProperty(position = 12, required = true, value = "Zip code", example = "10004")
@Override
public String getZip() {
return super.getZip();
}
@ApiModelProperty(position = 13, required = true, value = "Phone number", example = "+1(415)777-7777")
@Override
public String getPhone() {
return super.getPhone();
}
@ApiModelProperty(position = 14, required = true, value = "Email", example = "example@company.com")
@Override
public String getEmail() {
return super.getEmail();
}
@ApiModelProperty(position = 15, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode")
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();

5
common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java

@ -15,12 +15,15 @@
*/
package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@ApiModel
@Data
public class TenantInfo extends Tenant {
@ApiModelProperty(position = 15, value = "Tenant Profile name", example = "Default")
private String tenantProfileName;
public TenantInfo() {

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

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@ -31,18 +33,27 @@ import java.util.Optional;
import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class TenantProfile extends SearchTextBased<TenantProfileId> implements HasName {
@NoXss
@ApiModelProperty(position = 3, value = "Name of the tenant profile", example = "High Priority Tenants")
private String name;
@NoXss
@ApiModelProperty(position = 4, value = "Description of the tenant profile", example = "Any text")
private String description;
@ApiModelProperty(position = 5, value = "Default Tenant profile to be used.", example = "true")
private boolean isDefault;
@ApiModelProperty(position = 6, value = "If enabled, will push all messages related to this tenant and processed by core platform services into separate queue. " +
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true")
private boolean isolatedTbCore;
@ApiModelProperty(position = 7, value = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " +
"Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true")
private boolean isolatedTbRuleEngine;
@ApiModelProperty(position = 8, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.")
private transient TenantProfileData profileData;
@JsonIgnore
private byte[] profileDataBytes;
@ -65,6 +76,21 @@ public class TenantProfile extends SearchTextBased<TenantProfileId> implements H
this.setProfileData(tenantProfile.getProfileData());
}
@ApiModelProperty(position = 1, value = "JSON object with the tenant profile Id. " +
"Specify this field to update the tenant profile. " +
"Referencing non-existing tenant profile Id will cause error. " +
"Omit this field to create new tenant profile." )
@Override
public TenantProfileId getId() {
return super.getId();
}
@ApiModelProperty(position = 2, value = "Timestamp of the tenant profile creation, in milliseconds", example = "1609459200000", readOnly = true)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
@Override
public String getSearchText() {
return getName();

4
common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java

@ -15,17 +15,21 @@
*/
package org.thingsboard.server.common.data.rule;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
@ApiModel
@Data
@Slf4j
public class DefaultRuleChainCreateRequest implements Serializable {
private static final long serialVersionUID = 5600333716030561537L;
@ApiModelProperty(position = 1, required = true, value = "Name of the new rule chain", example = "Root Rule Chain")
private String name;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java

@ -15,14 +15,20 @@
*/
package org.thingsboard.server.common.data.rule;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* Created by ashvayka on 21.03.18.
*/
@ApiModel
@Data
public class NodeConnectionInfo {
@ApiModelProperty(position = 1, required = true, value = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.")
private int fromIndex;
@ApiModelProperty(position = 2, required = true, value = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'to' part of the connection.")
private int toIndex;
@ApiModelProperty(position = 3, required = true, value = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'")
private String type;
}

26
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@ -28,6 +30,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
@ -35,13 +38,20 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> im
private static final long serialVersionUID = -5656679015121935465L;
@ApiModelProperty(position = 3, required = true, value = "JSON object with Tenant Id.", readOnly = true)
private TenantId tenantId;
@NoXss
@ApiModelProperty(position = 4, required = true, value = "Rule Chain name", example = "Humidity data processing")
private String name;
@ApiModelProperty(position = 5, value = "Rule Chain type. 'EDGE' rule chains are processing messages on the edge devices only.", example = "A4B72CCDFF33")
private RuleChainType type;
@ApiModelProperty(position = 6, value = "JSON object with Rule Chain Id. Pointer to the first rule node that should receive all messages pushed to this rule chain.")
private RuleNodeId firstRuleNodeId;
@ApiModelProperty(position = 7, value = "Indicates root rule chain. The root rule chain process messages from all devices and entities by default. User may configure default rule chain per device profile.")
private boolean root;
@ApiModelProperty(position = 8, value = "Reserved for future usage.")
private boolean debugMode;
@ApiModelProperty(position = 9, value = "Reserved for future usage. The actual list of rule nodes and their relations is stored in the database separately.")
private transient JsonNode configuration;
@JsonIgnore
@ -75,6 +85,21 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> im
return name;
}
@ApiModelProperty(position = 1, value = "JSON object with the Rule Chain Id. " +
"Specify this field to update the Rule Chain. " +
"Referencing non-existing Rule Chain Id will cause error. " +
"Omit this field to create new rule chain." )
@Override
public RuleChainId getId() {
return super.getId();
}
@ApiModelProperty(position = 2, value = "Timestamp of the rule chain creation, in milliseconds", example = "1609459200000", readOnly = true)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
public JsonNode getConfiguration() {
return SearchTextBasedWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes);
}
@ -82,4 +107,5 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> im
public void setConfiguration(JsonNode data) {
setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes);
}
}

7
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java

@ -16,16 +16,23 @@
package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
/**
* Created by ashvayka on 21.03.18.
*/
@ApiModel
@Data
public class RuleChainConnectionInfo {
@ApiModelProperty(position = 1, required = true, value = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.")
private int fromIndex;
@ApiModelProperty(position = 2, required = true, value = "JSON object with the Rule Chain Id.")
private RuleChainId targetRuleChainId;
@ApiModelProperty(position = 3, required = true, value = "JSON object with the additional information about the connection.")
private JsonNode additionalInfo;
@ApiModelProperty(position = 4, required = true, value = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'")
private String type;
}

5
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java

@ -15,13 +15,18 @@
*/
package org.thingsboard.server.common.data.rule;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@ApiModel
@Data
public class RuleChainData {
@ApiModelProperty(position = 1, required = true, value = "List of the Rule Chain objects.", readOnly = true)
List<RuleChain> ruleChains;
@ApiModelProperty(position = 2, required = true, value = "List of the Rule Chain metadata objects.", readOnly = true)
List<RuleChainMetaData> metadata;
}

13
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java

@ -15,17 +15,22 @@
*/
package org.thingsboard.server.common.data.rule;
import lombok.AllArgsConstructor;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@Data
@AllArgsConstructor
public class RuleChainImportResult {
@JsonIgnore
private TenantId tenantId;
private RuleChainId ruleChainId;
private ComponentLifecycleEvent lifecycleEvent;
private String ruleChainName;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private boolean updated;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String error;
}

8
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java

@ -16,6 +16,8 @@
package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
@ -25,17 +27,23 @@ import java.util.List;
/**
* Created by igor on 3/13/18.
*/
@ApiModel
@Data
public class RuleChainMetaData {
@ApiModelProperty(position = 1, required = true, value = "JSON object with Rule Chain Id.", readOnly = true)
private RuleChainId ruleChainId;
@ApiModelProperty(position = 2, required = true, value = "Index of the first rule node in the 'nodes' list")
private Integer firstNodeIndex;
@ApiModelProperty(position = 3, required = true, value = "List of rule node JSON objects")
private List<RuleNode> nodes;
@ApiModelProperty(position = 4, required = true, value = "List of JSON objects that represent connections between rule nodes")
private List<NodeConnectionInfo> connections;
@ApiModelProperty(position = 5, required = true, value = "List of JSON objects that represent connections between rule nodes and other rule chains.")
private List<RuleChainConnectionInfo> ruleChainConnections;
public void addConnectionInfo(int fromIndex, int toIndex, String type) {

29
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@ -25,6 +27,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ApiModel
@Data
@EqualsAndHashCode(callSuper = true)
@Slf4j
@ -32,10 +35,15 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
private static final long serialVersionUID = -5656679015121235465L;
@ApiModelProperty(position = 3, value = "JSON object with the Rule Chain Id. ", readOnly = true)
private RuleChainId ruleChainId;
@ApiModelProperty(position = 4, value = "Full Java Class Name of the rule node implementation. ", example = "com.mycompany.iot.rule.engine.ProcessingNode")
private String type;
@ApiModelProperty(position = 5, value = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading")
private String name;
@ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false")
private boolean debugMode;
@ApiModelProperty(position = 7, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
private transient JsonNode configuration;
@JsonIgnore
private byte[] configurationBytes;
@ -75,4 +83,25 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes);
}
@ApiModelProperty(position = 1, value = "JSON object with the Rule Node Id. " +
"Specify this field to update the Rule Node. " +
"Referencing non-existing Rule Node Id will cause error. " +
"Omit this field to create new rule node." )
@Override
public RuleNodeId getId() {
return super.getId();
}
@ApiModelProperty(position = 2, value = "Timestamp of the rule node creation, in milliseconds", example = "1609459200000", readOnly = true)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
@ApiModelProperty(position = 8, value = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", dataType = "com.fasterxml.jackson.databind.JsonNode")
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
}

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

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

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

@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.annotations.ApiModel;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.TenantProfileType;

4
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileData.java

@ -15,11 +15,15 @@
*/
package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel
@Data
public class TenantProfileData {
@ApiModelProperty(position = 1, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.")
private TenantProfileConfiguration configuration;
}

28
common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2021 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.common.data.id;
import org.junit.Assert;
import org.junit.Test;
public class EntityIdTest {
@Test
public void givenConstantNullUuid_whenCompare_thenToStringEqualsPredefinedUuid() {
Assert.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString());
}
}

12
common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@ -53,4 +54,15 @@ public class DonAsynchron {
Futures.addCallback(future, callback, MoreExecutors.directExecutor());
}
}
public static <T> ListenableFuture<T> submit(Callable<T> task, Consumer<T> onSuccess, Consumer<Throwable> onFailure, Executor executor) {
return submit(task, onSuccess, onFailure, executor, null);
}
public static <T> ListenableFuture<T> submit(Callable<T> task, Consumer<T> onSuccess, Consumer<Throwable> onFailure, Executor executor, Executor callbackExecutor) {
ListenableFuture<T> future = Futures.submit(task, executor);
withCallback(future, onSuccess, onFailure, callbackExecutor);
return future;
}
}

22
dao/pom.xml

@ -201,6 +201,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
@ -211,6 +216,16 @@
<artifactId>hsqldb</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>jdbc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
@ -239,7 +254,14 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${surfire.version}</version>
<configuration>
<excludes>
<exclude>**/sql/*Test.java</exclude>
<exclude>**/sql/*/*Test.java</exclude>
<exclude>**/psql/*Test.java</exclude>
<exclude>**/nosql/*Test.java</exclude>
</excludes>
<includes>
<include>**/*Test.java</include>
<include>**/*TestSuite.java</include>
</includes>
</configuration>

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java

@ -35,6 +35,8 @@ public interface DeviceCredentialsDao extends Dao<DeviceCredentials> {
*/
DeviceCredentials save(TenantId tenantId, DeviceCredentials deviceCredentials);
DeviceCredentials saveAndFlush(TenantId tenantId, DeviceCredentials deviceCredentials);
/**
* Find device credentials by device id.
*

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -96,7 +96,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
credentialsValidator.validate(deviceCredentials, id -> tenantId);
try {
return deviceCredentialsDao.save(tenantId, deviceCredentials);
return deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials);
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java

@ -30,6 +30,8 @@ public interface DeviceProfileDao extends Dao<DeviceProfile> {
DeviceProfile save(TenantId tenantId, DeviceProfile deviceProfile);
DeviceProfile saveAndFlush(TenantId tenantId, DeviceProfile deviceProfile);
PageData<DeviceProfile> findDeviceProfiles(TenantId tenantId, PageLink pageLink);
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -167,7 +167,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
}
DeviceProfile savedDeviceProfile;
try {
savedDeviceProfile = deviceProfileDao.save(deviceProfile.getTenantId(), deviceProfile);
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
} catch (Exception t) {
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) {

63
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@ -38,7 +39,6 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
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.NodeConnectionInfo;
@ -59,6 +59,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantDao;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -416,41 +417,46 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
@Override
public List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite) {
public List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) {
List<RuleChainImportResult> importResults = new ArrayList<>();
setRandomRuleChainIds(ruleChainData);
resetRuleNodeIds(ruleChainData.getMetadata());
resetRuleChainMetadataTenantIds(tenantId, ruleChainData.getMetadata());
if (overwrite) {
List<RuleChain> persistentRuleChains = findAllTenantRuleChains(tenantId, type);
for (RuleChain ruleChain : ruleChainData.getRuleChains()) {
ComponentLifecycleEvent lifecycleEvent;
Optional<RuleChain> persistentRuleChainOpt = persistentRuleChains.stream().filter(rc -> rc.getName().equals(ruleChain.getName())).findFirst();
if (persistentRuleChainOpt.isPresent()) {
setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), ruleChain.getId(), persistentRuleChainOpt.get().getId());
ruleChain.setRoot(persistentRuleChainOpt.get().isRoot());
lifecycleEvent = ComponentLifecycleEvent.UPDATED;
} else {
ruleChain.setRoot(false);
lifecycleEvent = ComponentLifecycleEvent.CREATED;
for (RuleChain ruleChain : ruleChainData.getRuleChains()) {
RuleChainImportResult importResult = new RuleChainImportResult();
ruleChain.setTenantId(tenantId);
ruleChain.setRoot(false);
if (overwrite) {
Collection<RuleChain> existingRuleChains = ruleChainDao.findByTenantIdAndTypeAndName(tenantId,
Optional.ofNullable(ruleChain.getType()).orElse(RuleChainType.CORE), ruleChain.getName());
Optional<RuleChain> existingRuleChain = existingRuleChains.stream().findFirst();
if (existingRuleChain.isPresent()) {
setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), ruleChain.getId(), existingRuleChain.get().getId());
ruleChain.setRoot(existingRuleChain.get().isRoot());
importResult.setUpdated(true);
}
ruleChain.setTenantId(tenantId);
ruleChainDao.save(tenantId, ruleChain);
importResults.add(new RuleChainImportResult(tenantId, ruleChain.getId(), lifecycleEvent));
}
} else {
if (!CollectionUtils.isEmpty(ruleChainData.getRuleChains())) {
ruleChainData.getRuleChains().forEach(rc -> {
rc.setTenantId(tenantId);
rc.setRoot(false);
RuleChain savedRc = ruleChainDao.save(tenantId, rc);
importResults.add(new RuleChainImportResult(tenantId, savedRc.getId(), ComponentLifecycleEvent.CREATED));
});
try {
ruleChain = saveRuleChain(ruleChain);
} catch (Exception e) {
importResult.setError(ExceptionUtils.getRootCauseMessage(e));
}
importResult.setTenantId(tenantId);
importResult.setRuleChainId(ruleChain.getId());
importResult.setRuleChainName(ruleChain.getName());
importResults.add(importResult);
}
if (!CollectionUtils.isEmpty(ruleChainData.getMetadata())) {
if (CollectionUtils.isNotEmpty(ruleChainData.getMetadata())) {
ruleChainData.getMetadata().forEach(md -> saveRuleChainMetaData(tenantId, md));
}
return importResults;
}
@ -475,7 +481,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
if (isTenantId) {
ObjectNode objNode = (ObjectNode) node;
objNode.put("id", tenantId.getId().toString());
if (objNode.has("id")) {
objNode.put("id", tenantId.getId().toString());
}
} else {
for (JsonNode jsonNode : node) {
searchTenantIdRecursive(tenantId, jsonNode);
@ -726,4 +734,5 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
checkRuleNodesAndDelete(tenantId, entity.getId());
}
};
}

5
dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.rule;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
@ -22,6 +23,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.TenantEntityDao;
import java.util.Collection;
import java.util.UUID;
/**
@ -74,4 +76,7 @@ public interface RuleChainDao extends Dao<RuleChain>, TenantEntityDao {
* @return the list of rule chain objects
*/
PageData<RuleChain> findAutoAssignToEdgeRuleChainsByTenantId(UUID tenantId, PageLink pageLink);
Collection<RuleChain> findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name);
}

4
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.dao.sql.device;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity;
import java.util.UUID;
@ -23,7 +23,7 @@ import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/6/2017.
*/
public interface DeviceCredentialsRepository extends CrudRepository<DeviceCredentialsEntity, UUID> {
public interface DeviceCredentialsRepository extends JpaRepository<DeviceCredentialsEntity, UUID> {
DeviceCredentialsEntity findByDeviceId(UUID deviceId);

3
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.device;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
@ -26,7 +27,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
import java.util.UUID;
public interface DeviceProfileRepository extends PagingAndSortingRepository<DeviceProfileEntity, UUID> {
public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEntity, UUID> {
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d " +

9
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.device;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.DaoUtil;
@ -46,6 +47,14 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao<DeviceCredentialsEnt
return deviceCredentialsRepository;
}
@Transactional
@Override
public DeviceCredentials saveAndFlush(TenantId tenantId, DeviceCredentials deviceCredentials) {
DeviceCredentials result = save(tenantId, deviceCredentials);
deviceCredentialsRepository.flush();
return result;
}
@Override
public DeviceCredentials findByDeviceId(TenantId tenantId, UUID deviceId) {
return DaoUtil.getData(deviceCredentialsRepository.findByDeviceId(deviceId));

9
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java

@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
@ -54,6 +55,14 @@ public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao<DeviceProfileE
return deviceProfileRepository.findDeviceProfileInfoById(deviceProfileId);
}
@Transactional
@Override
public DeviceProfile saveAndFlush(TenantId tenantId, DeviceProfile deviceProfile) {
DeviceProfile result = save(tenantId, deviceProfile);
deviceProfileRepository.flush();
return result;
}
@Override
public PageData<DeviceProfile> findDeviceProfiles(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(

44
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -15,8 +15,10 @@
*/
package org.thingsboard.server.dao.sql.query;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.support.TransactionTemplate;
@ -239,22 +241,38 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{
EntityType.TENANT, EntityType.CUSTOMER, EntityType.USER, EntityType.DASHBOARD, EntityType.ASSET, EntityType.DEVICE, EntityType.ENTITY_VIEW};
private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, relation_type, lvl) AS (" +
" SELECT from_id, from_type, to_id, to_type, relation_type, 1 as lvl" +
" FROM relation" +
private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, lvl, path) AS (" +
" SELECT from_id, from_type, to_id, to_type," +
" 1 as lvl," +
" ARRAY[$in_id] as path" + // initial path
" FROM relation " +
" WHERE $in_id = :relation_root_id and $in_type = :relation_root_type and relation_type_group = 'COMMON'" +
" GROUP BY from_id, from_type, to_id, to_type, lvl, path" +
" UNION ALL" +
" SELECT r.from_id, r.from_type, r.to_id, r.to_type, r.relation_type, lvl + 1" +
" SELECT r.from_id, r.from_type, r.to_id, r.to_type," +
" (re.lvl + 1) as lvl, " +
" (re.path || ARRAY[r.$in_id]) as path" +
" FROM relation r" +
" INNER JOIN related_entities re ON" +
" r.$in_id = re.$out_id and r.$in_type = re.$out_type and" +
" relation_type_group = 'COMMON' %s)" +
" SELECT re.$out_id entity_id, re.$out_type entity_type, max(re.lvl) lvl" +
" from related_entities re" +
" relation_type_group = 'COMMON' " +
" AND r.$in_id NOT IN (SELECT * FROM unnest(re.path)) " +
" %s" +
" GROUP BY r.from_id, r.from_type, r.to_id, r.to_type, (re.lvl + 1), (re.path || ARRAY[r.$in_id])" +
" )" +
" SELECT re.$out_id entity_id, re.$out_type entity_type, max(r_int.lvl) lvl" +
" from related_entities r_int" +
" INNER JOIN relation re ON re.from_id = r_int.from_id AND re.from_type = r_int.from_type" +
" AND re.to_id = r_int.to_id AND re.to_type = r_int.to_type" +
" AND re.relation_type_group = 'COMMON'" +
" %s GROUP BY entity_id, entity_type) entity";
private static final String HIERARCHICAL_TO_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "to").replace("$out", "from");
private static final String HIERARCHICAL_FROM_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "from").replace("$out", "to");
@Getter
@Value("${sql.relations.max_level:50}")
int maxLevelAllowed; //This value has to be reasonable small to prevent infinite recursion as early as possible
private final NamedParameterJdbcTemplate jdbcTemplate;
private final TransactionTemplate transactionTemplate;
private final DefaultQueryLogComponent queryLog;
@ -580,7 +598,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
.append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type");
notExistsPart.append(")");
whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")";
whereFilter += " and ( r_int.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")";
}
from = String.format(from, lvlFilter, whereFilter);
String query = "( " + selectFields + from + ")";
@ -659,7 +677,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
.append(whereFilter.toString().replaceAll("re\\.", "nr\\."));
notExistsPart.append(")");
whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")");
whereFilter.append(" and ( r_int.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")");
}
from = String.format(from, lvlFilter, " WHERE " + whereFilter);
return "( " + selectFields + from + ")";
@ -693,8 +711,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
return whereFilter.toString();
}
private String getLvlFilter(int maxLevel) {
return maxLevel > 0 ? ("and lvl <= " + (maxLevel - 1)) : "";
String getLvlFilter(int maxLevel) {
return "and re.lvl <= " + (getMaxLevel(maxLevel) - 1);
}
int getMaxLevel(int maxLevel) {
return (maxLevel <= 0 || maxLevel > this.maxLevelAllowed) ? this.maxLevelAllowed : maxLevel;
}
private String getQueryTemplate(EntitySearchDirection direction) {

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

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.model.sql.RuleChainEntity;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
import java.util.Collection;
import java.util.Objects;
import java.util.UUID;
@ -97,8 +98,14 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao<RuleChainEntity, R
DaoUtil.toPageable(pageLink)));
}
@Override
public Collection<RuleChain> findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name) {
return DaoUtil.convertDataList(ruleChainRepository.findByTenantIdAndTypeAndName(tenantId.getId(), type, name));
}
@Override
public Long countByTenantId(TenantId tenantId) {
return ruleChainRepository.countByTenantId(tenantId.getId());
}
}

8
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java

@ -23,6 +23,7 @@ import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.dao.model.sql.RuleChainEntity;
import java.util.List;
import java.util.UUID;
public interface RuleChainRepository extends PagingAndSortingRepository<RuleChainEntity, UUID> {
@ -55,10 +56,13 @@ public interface RuleChainRepository extends PagingAndSortingRepository<RuleChai
"AND re.relationType = 'Contains' AND re.fromId = :tenantId AND re.fromType = 'TENANT' " +
"AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
Page<RuleChainEntity> findAutoAssignByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
Pageable pageable);
@Param("searchText") String searchText,
Pageable pageable);
RuleChainEntity findByTenantIdAndTypeAndRootIsTrue(UUID tenantId, RuleChainType ruleChainType);
Long countByTenantId(UUID tenantId);
List<RuleChainEntity> findByTenantIdAndTypeAndName(UUID tenantId, RuleChainType type, String name);
}

30
dao/src/test/java/org/thingsboard/server/dao/PostgreSqlDaoServiceTestSuite.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2021 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.dao;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
"org.thingsboard.server.dao.service.psql.*SqlTest",
"org.thingsboard.server.dao.service.attributes.psql.*SqlTest",
"org.thingsboard.server.dao.service.event.psql.*SqlTest",
"org.thingsboard.server.dao.service.timeseries.psql.*SqlTest"
})
public class PostgreSqlDaoServiceTestSuite {
}

64
dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2021 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.dao;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
@Slf4j
public class PostgreSqlInitializer {
private static final List<String> sqlFiles = List.of(
"sql/schema-ts-psql.sql",
"sql/schema-entities.sql",
"sql/schema-entities-idx.sql",
"sql/system-data.sql",
"sql/system-test-psql.sql");
private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql";
public static void initDb(Connection conn) {
cleanUpDb(conn);
log.info("initialize Postgres DB...");
try {
for (String sqlFile : sqlFiles) {
URL sqlFileUrl = Resources.getResource(sqlFile);
String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8);
conn.createStatement().execute(sql);
}
} catch (IOException | SQLException e) {
throw new RuntimeException("Unable to init the Postgres database. Reason: " + e.getMessage(), e);
}
log.info("Postgres DB is initialized!");
}
private static void cleanUpDb(Connection conn) {
log.info("clean up Postgres DB...");
try {
URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile);
String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8);
conn.createStatement().execute(dropAllTablesSql);
} catch (IOException | SQLException e) {
throw new RuntimeException("Unable to clean up the Postgres database. Reason: " + e.getMessage(), e);
}
}
}

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java

@ -47,7 +47,7 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
public class BaseDeviceProfileServiceTest extends AbstractServiceTest {
public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest {
private IdComparator<DeviceProfile> idComparator = new IdComparator<>();
private IdComparator<DeviceProfileInfo> deviceProfileInfoIdComparator = new IdComparator<>();

200
dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java

@ -17,14 +17,17 @@ package org.thingsboard.server.dao.service;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
@ -69,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.sql.relation.RelationRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.ArrayList;
@ -82,9 +86,13 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
@Slf4j
public abstract class BaseEntityServiceTest extends AbstractServiceTest {
static final int ENTITY_COUNT = 5;
@Autowired
private AttributesService attributesService;
@ -96,6 +104,9 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
@Autowired
private JdbcTemplate template;
@Autowired
private RelationRepository relationRepository;
@Before
public void before() {
Tenant tenant = new Tenant();
@ -110,7 +121,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
tenantService.deleteTenant(tenantId);
}
@Test
public void testCountEntitiesByQuery() throws InterruptedException {
List<Device> devices = new ArrayList<>();
@ -154,12 +165,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, count);
}
@Test
public void testCountHierarchicalEntitiesByQuery() throws InterruptedException {
List<Asset> assets = new ArrayList<>();
List<Device> devices = new ArrayList<>();
createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId);
@ -168,7 +179,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
EntityCountQuery countQuery = new EntityCountQuery(filter);
long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
Assert.assertEquals(30, count);
Assert.assertEquals(31, count); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31)
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery);
@ -304,11 +315,25 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
@Test
public void testHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException {
doTestHierarchicalFindEntityDataWithAttributesByQuery(0, false);
}
@Test
public void testHierarchicalFindEntityDataWithAttributesByQueryWithLevel() throws ExecutionException, InterruptedException {
doTestHierarchicalFindEntityDataWithAttributesByQuery(2, false);
}
@Test
public void testHierarchicalFindEntityDataWithAttributesByQueryWithLastLevelOnly() throws ExecutionException, InterruptedException {
doTestHierarchicalFindEntityDataWithAttributesByQuery(2, true);
}
private void doTestHierarchicalFindEntityDataWithAttributesByQuery(final int maxLevel, final boolean fetchLastLevelOnly) throws ExecutionException, InterruptedException {
List<Asset> assets = new ArrayList<>();
List<Device> devices = new ArrayList<>();
List<Long> temperatures = new ArrayList<>();
List<Long> highTemperatures = new ArrayList<>();
createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
List<ListenableFuture<List<Void>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) {
@ -321,6 +346,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
filter.setRootEntity(tenantId);
filter.setDirection(EntitySearchDirection.FROM);
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
filter.setMaxLevel(maxLevel);
filter.setFetchLastLevelOnly(fetchLastLevelOnly);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
@ -373,14 +400,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
deviceService.deleteDevicesByTenantId(tenantId);
}
@Test
public void testHierarchicalFindDevicesWithAttributesByQuery() throws ExecutionException, InterruptedException {
List<Asset> assets = new ArrayList<>();
List<Device> devices = new ArrayList<>();
List<Long> temperatures = new ArrayList<>();
List<Long> highTemperatures = new ArrayList<>();
createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures);
List<ListenableFuture<List<Void>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < devices.size(); i++) {
@ -393,6 +419,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
filter.setRootEntity(tenantId);
filter.setDirection(EntitySearchDirection.FROM);
filter.setRelationType("Contains");
filter.setMaxLevel(2);
filter.setFetchLastLevelOnly(true);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
@ -446,14 +474,14 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
deviceService.deleteDevicesByTenantId(tenantId);
}
@Test
public void testHierarchicalFindAssetsWithAttributesByQuery() throws ExecutionException, InterruptedException {
List<Asset> assets = new ArrayList<>();
List<Device> devices = new ArrayList<>();
List<Long> consumptions = new ArrayList<>();
List<Long> highConsumptions = new ArrayList<>();
createTestHierarchy(assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>());
createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>());
List<ListenableFuture<List<Void>>> attributeFutures = new ArrayList<>();
for (int i = 0; i < assets.size(); i++) {
@ -518,8 +546,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
deviceService.deleteDevicesByTenantId(tenantId);
}
private void createTestHierarchy(List<Asset> assets, List<Device> devices, List<Long> consumptions, List<Long> highConsumptions, List<Long> temperatures, List<Long> highTemperatures) throws InterruptedException {
for (int i = 0; i < 5; i++) {
private void createTestHierarchy(TenantId tenantId, List<Asset> assets, List<Device> devices, List<Long> consumptions, List<Long> highConsumptions, List<Long> temperatures, List<Long> highTemperatures) throws InterruptedException {
for (int i = 0; i < ENTITY_COUNT; i++) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setName("Asset" + i);
@ -529,18 +557,19 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
//TO make sure devices have different created time
Thread.sleep(1);
assets.add(asset);
EntityRelation er = new EntityRelation();
er.setFrom(tenantId);
er.setTo(asset.getId());
er.setType("Manages");
er.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(tenantId, er);
createRelation(tenantId, "Manages", tenantId, asset.getId());
long consumption = (long) (Math.random() * 100);
consumptions.add(consumption);
if (consumption > 50) {
highConsumptions.add(consumption);
}
for (int j = 0; j < 5; j++) {
//tenant -> asset : one-to-one but many edges
for (int n = 0; n < ENTITY_COUNT; n++) {
createRelation(tenantId, "UseCase-" + n, tenantId, asset.getId());
}
for (int j = 0; j < ENTITY_COUNT; j++) {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("A" + i + "Device" + j);
@ -550,22 +579,125 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
//TO make sure devices have different created time
Thread.sleep(1);
devices.add(device);
er = new EntityRelation();
er.setFrom(asset.getId());
er.setTo(device.getId());
er.setType("Contains");
er.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(tenantId, er);
createRelation(tenantId, "Contains", asset.getId(), device.getId());
long temperature = (long) (Math.random() * 100);
temperatures.add(temperature);
if (temperature > 45) {
highTemperatures.add(temperature);
}
//asset -> device : one-to-one but many edges
for (int n = 0; n < ENTITY_COUNT; n++) {
createRelation(tenantId, "UseCase-" + n, asset.getId(), device.getId());
}
}
}
//asset -> device one-to-many shared with other assets
for (int n = 0; n < devices.size(); n = n + ENTITY_COUNT) {
createRelation(tenantId, "SharedWithAsset0", assets.get(0).getId(), devices.get(n).getId());
}
createManyCustomRelationsBetweenTwoNodes(tenantId, "UseCase", assets, devices);
createHorizontalRingRelations(tenantId, "Ring(Loop)-Ast", assets);
createLoopRelations(tenantId, "Loop-Tnt-Ast-Dev", tenantId, assets.get(0).getId(), devices.get(0).getId());
createLoopRelations(tenantId, "Loop-Tnt-Ast", tenantId, assets.get(1).getId());
createLoopRelations(tenantId, "Loop-Ast-Tnt-Ast", assets.get(2).getId(), tenantId, assets.get(3).getId());
//printAllRelations();
}
private ResultSetExtractor<List<List<String>>> getListResultSetExtractor() {
return rs -> {
List<List<String>> list = new ArrayList<>();
final int columnCount = rs.getMetaData().getColumnCount();
List<String> columns = new ArrayList<>(columnCount);
for (int i = 1; i <= columnCount; i++) {
columns.add(rs.getMetaData().getColumnName(i));
}
list.add(columns);
while (rs.next()) {
List<String> data = new ArrayList<>(columnCount);
for (int i = 1; i <= columnCount; i++) {
data.add(rs.getString(i));
}
list.add(data);
}
return list;
};
}
/*
* This useful to reproduce exact data in the PostgreSQL and play around with pgadmin query and analyze tool
* */
private void printAllRelations() {
System.out.println("" +
"DO\n" +
"$$\n" +
" DECLARE\n" +
" someint integer;\n" +
" BEGIN\n" +
" DROP TABLE IF EXISTS relation_test;\n" +
" CREATE TABLE IF NOT EXISTS relation_test\n" +
" (\n" +
" from_id uuid,\n" +
" from_type varchar(255),\n" +
" to_id uuid,\n" +
" to_type varchar(255),\n" +
" relation_type_group varchar(255),\n" +
" relation_type varchar(255),\n" +
" additional_info varchar,\n" +
" CONSTRAINT relation_test_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)\n" +
" );");
relationRepository.findAll().forEach(r ->
System.out.printf("INSERT INTO relation_test (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" +
" VALUES (%s, %s, %s, %s, %s, %s, %s);\n",
quote(r.getFromId()), quote(r.getFromType()), quote(r.getToId()), quote(r.getToType()),
quote(r.getRelationTypeGroup()), quote(r.getRelationType()), quote(r.getAdditionalInfo()))
);
System.out.println("" +
" END\n" +
"$$;");
}
private String quote(Object s) {
return s == null ? null : "'" + s + "'";
}
void createLoopRelations(TenantId tenantId, String type, EntityId... ids) {
assertThat("ids lenght", ids.length, Matchers.greaterThanOrEqualTo(1));
//chain all from the head to the tail
for (int i = 1; i < ids.length; i++) {
relationService.saveRelation(tenantId, new EntityRelation(ids[i - 1], ids[i], type, RelationTypeGroup.COMMON));
}
//chain tail -> head
relationService.saveRelation(tenantId, new EntityRelation(ids[ids.length - 1], ids[0], type, RelationTypeGroup.COMMON));
}
void createHorizontalRingRelations(TenantId tenantId, String type, List<Asset> assets) {
createLoopRelations(tenantId, type, assets.stream().map(Asset::getId).toArray(EntityId[]::new));
}
void createManyCustomRelationsBetweenTwoNodes(TenantId tenantId, String type, List<Asset> assets, List<Device> devices) {
for (int i = 1; i <= 5; i++) {
final String typeI = type + i;
createOneToManyRelations(tenantId, typeI, tenantId, assets.stream().map(Asset::getId).collect(Collectors.toList()));
assets.forEach(asset ->
createOneToManyRelations(tenantId, typeI, asset.getId(), devices.stream().map(Device::getId).collect(Collectors.toList())));
}
}
void createOneToManyRelations(TenantId tenantId, String type, EntityId from, List<EntityId> toIds) {
toIds.forEach(toId -> createRelation(tenantId, type, from, toId));
}
void createRelation(TenantId tenantId, String type, EntityId from, EntityId toId) {
relationService.saveRelation(tenantId, new EntityRelation(from, toId, type, RelationTypeGroup.COMMON));
}
@Test
public void testSimpleFindEntityDataByQuery() throws InterruptedException {
List<Device> devices = new ArrayList<>();
@ -871,7 +1003,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
@Test
public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException{
public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException {
List<Device> devices = new ArrayList<>();
List<Long> temperatures = new ArrayList<>();
@ -1031,7 +1163,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
deviceService.deleteDevicesByTenantId(tenantId);
}
@Test
public void testFindEntityDataByQueryWithTimeseries() throws ExecutionException, InterruptedException {
@ -1122,7 +1254,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
@Test
public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException{
public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException {
List<Device> devices = new ArrayList<>();
List<String> attributeStrings = new ArrayList<>();
@ -1142,11 +1274,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
devices.add(deviceService.saveDevice(device));
//TO make sure devices have different created time
Thread.sleep(1);
List<StringFilterPredicate.StringOperation> operationValues= Arrays.asList(StringFilterPredicate.StringOperation.values());
List<StringFilterPredicate.StringOperation> operationValues = Arrays.asList(StringFilterPredicate.StringOperation.values());
StringFilterPredicate.StringOperation operation = operationValues.get(new Random().nextInt(operationValues.size()));
String operationName = operation.name();
attributeStrings.add(operationName);
switch(operation){
switch (operation) {
case EQUAL:
equalStrings.add(operationName);
notContainsStrings.add(operationName);
@ -1302,7 +1434,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
@Test
public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException{
public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException {
List<Device> devices = new ArrayList<>();
@ -1419,7 +1551,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
}
@Test
public void testBuildSimplePredicateQueryOperations() throws InterruptedException{
public void testBuildSimplePredicateQueryOperations() throws InterruptedException {
List<Device> devices = new ArrayList<>();
@ -1492,7 +1624,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
return loadedEntities;
}
private List<KeyFilter> createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value){
private List<KeyFilter> createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value) {
KeyFilter filter = new KeyFilter();
filter.setKey(new EntityKey(keyType, key));
StringFilterPredicate predicate = new StringFilterPredicate();
@ -1503,7 +1635,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
return Collections.singletonList(filter);
}
private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value){
private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value) {
KeyFilter filter = new KeyFilter();
filter.setKey(new EntityKey(keyType, key));
NumericFilterPredicate predicate = new NumericFilterPredicate();

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java

@ -31,7 +31,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import java.util.Arrays;
import java.util.UUID;
public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
public abstract class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
@Autowired
protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java

@ -43,7 +43,7 @@ import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class BaseOAuth2ServiceTest extends AbstractServiceTest {
public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest {
private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, Collections.emptyList());
@Autowired

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java

@ -34,7 +34,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class BaseTenantProfileServiceTest extends AbstractServiceTest {
public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest {
private IdComparator<TenantProfile> idComparator = new IdComparator<>();
private IdComparator<EntityInfo> tenantProfileInfoIdComparator = new IdComparator<>();

33
dao/src/test/java/org/thingsboard/server/dao/service/DaoPostgreSqlTest.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2021 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.dao.service;
import org.springframework.test.context.TestPropertySource;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:psql-test.properties"})
public @interface DaoPostgreSqlTest {
}

8
dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java → dao/src/test/java/org/thingsboard/server/dao/service/psql/EntityServicePostgreSqlTest.java

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.sql;
package org.thingsboard.server.dao.service.psql;
import org.thingsboard.server.dao.service.BaseEntityServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.service.DaoPostgreSqlTest;
@DaoSqlTest
public class EntityServiceSqlTest extends BaseEntityServiceTest {
@DaoPostgreSqlTest
public class EntityServicePostgreSqlTest extends BaseEntityServiceTest {
}

69
dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepositoryTest.java

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2021 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.dao.sql.query;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.support.TransactionTemplate;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = DefaultEntityQueryRepository.class)
public class DefaultEntityQueryRepositoryTest {
@MockBean
NamedParameterJdbcTemplate jdbcTemplate;
@MockBean
TransactionTemplate transactionTemplate;
@MockBean
DefaultQueryLogComponent queryLog;
@Autowired
DefaultEntityQueryRepository repo;
/*
* This value has to be reasonable small to prevent infinite recursion as early as possible
* */
@Test
public void givenDefaultMaxLevel_whenStaticConstant_thenEqualsTo() {
assertThat(repo.getMaxLevelAllowed(), equalTo(50));
}
@Test
public void givenMaxLevelZeroOrNegative_whenGetMaxLevel_thenReturnDefaultMaxLevel() {
assertThat(repo.getMaxLevel(0), equalTo(repo.getMaxLevelAllowed()));
assertThat(repo.getMaxLevel(-1), equalTo(repo.getMaxLevelAllowed()));
assertThat(repo.getMaxLevel(-2), equalTo(repo.getMaxLevelAllowed()));
assertThat(repo.getMaxLevel(Integer.MIN_VALUE), equalTo(repo.getMaxLevelAllowed()));
}
@Test
public void givenMaxLevelPositive_whenGetMaxLevel_thenValueTheSame() {
assertThat(repo.getMaxLevel(1), equalTo(1));
assertThat(repo.getMaxLevel(2), equalTo(2));
assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed()), equalTo(repo.getMaxLevelAllowed()));
assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed() + 1), equalTo(repo.getMaxLevelAllowed()));
assertThat(repo.getMaxLevel(Integer.MAX_VALUE), equalTo(repo.getMaxLevelAllowed()));
}
}

47
dao/src/test/resources/psql-test.properties

@ -0,0 +1,47 @@
database.ts.type=sql
database.ts_latest.type=sql
sql.ts_inserts_executor_type=fixed
sql.ts_inserts_fixed_thread_pool_size=200
sql.ts_key_value_partitioning=MONTHS
#
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.order_by.default_null_ordering=last
spring.jpa.properties.hibernate.jdbc.log.warnings=false
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
#org.postgresql.Driver
spring.datasource.hikari.maximumPoolSize=50
service.type=monolith
#database.ts.type=timescale
#database.ts.type=sql
#database.entities.type=sql
#
#sql.ts_inserts_executor_type=fixed
#sql.ts_inserts_fixed_thread_pool_size=200
#sql.ts_key_value_partitioning=MONTHS
#
#spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
#spring.jpa.show-sql=false
#spring.jpa.hibernate.ddl-auto=none
#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
#
#spring.datasource.username=postgres
#spring.datasource.password=postgres
#spring.datasource.url=jdbc:postgresql://localhost:5432/sqltest
#spring.datasource.driverClassName=org.postgresql.Driver
#spring.datasource.hikari.maximumPoolSize = 50
queue.core.pack-processing-timeout=3000
queue.rule-engine.pack-processing-timeout=3000
queue.rule-engine.queues[0].name=Main
queue.rule-engine.queues[0].topic=tb_rule_engine.main
queue.rule-engine.queues[0].poll-interval=25
queue.rule-engine.queues[0].partitions=3
queue.rule-engine.queues[0].pack-processing-timeout=3000
queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES
queue.rule-engine.queues[0].submit-strategy.type=BURST
sql.log_entity_queries=true

2
dao/src/test/resources/sql/system-test-psql.sql

@ -0,0 +1,2 @@
--PostgreSQL specific truncate to fit constraints
TRUNCATE TABLE device_credentials, device, device_profile, rule_node_state, rule_node, rule_chain;

11
pom.xml

@ -1643,6 +1643,17 @@
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>jdbc</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java

@ -20,6 +20,18 @@ import lombok.Data;
@Data
public abstract class TbAbstractAlarmNodeConfiguration {
static final String ALARM_DETAILS_BUILD_JS_TEMPLATE = "" +
"var details = {};\n" +
"if (metadata.prevAlarmDetails) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
" //remove prevAlarmDetails from metadata\n" +
" delete metadata.prevAlarmDetails;\n" +
" //now metadata is the same as it comes IN this rule node\n" +
"}\n" +
"\n" +
"\n" +
"return details;";
private String alarmType;
private String alarmDetailsBuildJs;

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java

@ -25,11 +25,7 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat
@Override
public TbClearAlarmNodeConfiguration defaultConfiguration() {
TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
configuration.setAlarmDetailsBuildJs("var details = {};\n" +
"if (metadata.prevAlarmDetails) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
"}\n" +
"return details;");
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmType("General Alarm");
return configuration;
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -35,11 +35,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
@Override
public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setAlarmDetailsBuildJs("var details = {};\n" +
"if (metadata.prevAlarmDetails) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
"}\n" +
"return details;");
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL.name());
configuration.setPropagate(false);

68
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md

@ -0,0 +1,68 @@
#### Clear alarm details builder function
<div class="divider"></div>
<br/>
*function Details(msg, metadata, msgType): any*
JavaScript function generating **Alarm Details** object to update existing one. Used for storing additional parameters inside Alarm.<br>
For example you can save attribute name/value pair from Original Message payload or Metadata.
**Parameters:**
{% include rulenode/common_node_script_args %}
**Returns:**
Should return the object presenting **Alarm Details**.
Current Alarm Details can be accessed via `metadata.prevAlarmDetails`.<br>
**Note** that `metadata.prevAlarmDetails` is a raw String field, and it needs to be converted into object using this construction:
```javascript
var details = {};
if (metadata.prevAlarmDetails) {
// remove prevAlarmDetails from metadata
delete metadata.prevAlarmDetails;
details = JSON.parse(metadata.prevAlarmDetails);
}
{:copy-code}
```
<div class="divider"></div>
##### Examples
<ul>
<li>
Take <code>count</code> property from previous Alarm and increment it.<br>
Also put <code>temperature</code> attribute from inbound Message payload into Alarm details:
</li>
</ul>
```javascript
var details = {temperature: msg.temperature, count: 1};
if (metadata.prevAlarmDetails) {
var prevDetails = JSON.parse(metadata.prevAlarmDetails);
// remove prevAlarmDetails from metadata
delete metadata.prevAlarmDetails;
if (prevDetails.count) {
details.count = prevDetails.count + 1;
}
}
return details;
{:copy-code}
```
<br>
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/).
You can see the real life example, where this node is used, in the next tutorial:
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/)
<br>
<br>

8
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/common_node_script_args.md

@ -0,0 +1,8 @@
<ul>
<li><b>msg:</b> <code>{[key: string]: any}</code> - is a Message payload key/value object.
</li>
<li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value object.
</li>
<li><b>msgType:</b> <code>string</code> - is a string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values.
</li>
</ul>

69
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/create_alarm_node_script_fn.md

@ -0,0 +1,69 @@
#### Create alarm details builder function
<div class="divider"></div>
<br/>
*function Details(msg, metadata, msgType): any*
JavaScript function generating **Alarm Details** object. Used for storing additional parameters inside Alarm.<br>
For example you can save attribute name/value pair from Original Message payload or Metadata.
**Parameters:**
{% include rulenode/common_node_script_args %}
**Returns:**
Should return the object presenting **Alarm Details**.
**Optional:** previous Alarm Details can be accessed via `metadata.prevAlarmDetails`.<br>
If previous Alarm does not exist, this field will not be present in Metadata. **Note** that `metadata.prevAlarmDetails`<br>
is a raw String field, and it needs to be converted into object using this construction:
```javascript
var details = {};
if (metadata.prevAlarmDetails) {
// remove prevAlarmDetails from metadata
delete metadata.prevAlarmDetails;
details = JSON.parse(metadata.prevAlarmDetails);
}
{:copy-code}
```
<div class="divider"></div>
##### Examples
<ul>
<li>
Take <code>count</code> property from previous Alarm and increment it.<br>
Also put <code>temperature</code> attribute from inbound Message payload into Alarm details:
</li>
</ul>
```javascript
var details = {temperature: msg.temperature, count: 1};
if (metadata.prevAlarmDetails) {
var prevDetails = JSON.parse(metadata.prevAlarmDetails);
// remove prevAlarmDetails from metadata
delete metadata.prevAlarmDetails;
if (prevDetails.count) {
details.count = prevDetails.count + 1;
}
}
return details;
{:copy-code}
```
<br>
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/).
You can see the real life example, where this node is used, in the next tutorial:
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/)
<br>
<br>

69
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/filter_node_script_fn.md

@ -0,0 +1,69 @@
#### Filter message function
<div class="divider"></div>
<br/>
*function Filter(msg, metadata, msgType): boolean*
JavaScript function evaluating **true/false** condition on incoming Message.
**Parameters:**
{% include rulenode/common_node_script_args %}
**Returns:**
Should return `boolean` value. If `true` - send Message via **True** chain, otherwise **False** chain is used.
<div class="divider"></div>
##### Examples
* Forward all messages with `temperature` value greater than `20` to the **True** chain and all other messages to the **False** chain:
```javascript
return msg.temperature > 20;
{:copy-code}
```
* Forward all messages with type `ATTRIBUTES_UPDATED` to the **True** chain and all other messages to the **False** chain:
```javascript
if (msgType === 'ATTRIBUTES_UPDATED') {
return true;
} else {
return false;
}
{:copy-code}
```
<ul>
<li>Send message to the <strong>True</strong> chain if the following conditions are met.<br>Message type is <code>POST_TELEMETRY_REQUEST</code> and<br>
(device type is <code>vehicle</code> and <code>humidity</code> value is greater than <code>50</code> or<br>
device type is <code>controller</code> and <code>temperature</code> value is greater than <code>20</code> and <code>humidity</code> value is greater than <code>60</code>).<br>
Otherwise send message to the <strong>False</strong> chain:
</li>
</ul>
```javascript
if (msgType === 'POST_TELEMETRY_REQUEST') {
if (metadata.deviceType === 'vehicle') {
return msg.humidity > 50;
} else if (metadata.deviceType === 'controller') {
return msg.temperature > 20 && msg.humidity > 60;
}
}
return false;
{:copy-code}
```
<br>
You can see real life example, how to use this node in those tutorials:
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/#node-a-filter-script)
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-filter-script-node)
<br>
<br>

118
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/generator_node_script_fn.md

@ -0,0 +1,118 @@
#### Message generator function
<div class="divider"></div>
<br/>
*function Generate(prevMsg, prevMetadata, prevMsgType): {msg: object, metadata: object, msgType: string}*
JavaScript function generating new message using previous Message payload, Metadata and Message type as input arguments.
**Parameters:**
<ul>
<li><b>prevMsg:</b> <code>{[key: string]: any}</code> - is a previously generated Message payload key/value object.
</li>
<li><b>prevMetadata:</b> <code>{[key: string]: string}</code> - is a previously generated Message metadata key/value object.
</li>
<li><b>prevMsgType:</b> <code>string</code> - is a previously generated string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values.
</li>
</ul>
**Returns:**
Should return the object with the following structure:
```javascript
{
msg?: {[key: string]: any},
metadata?: {[key: string]: string},
msgType?: string
}
```
All fields in resulting object are optional and will be taken from previously generated Message if not specified.
<div class="divider"></div>
##### Examples
* Generate message of type `POST_TELEMETRY_REQUEST` with random `temperature` value from `18` to `32`:
```javascript
var temperature = 18 + Math.random() * 14;
// Round to at most 2 decimal places (optional)
temperature = Math.round( temperature * 100 ) / 100;
var msg = { temperature: temperature };
var metadata = {};
var msgType = "POST_TELEMETRY_REQUEST";
return { msg: msg, metadata: metadata, msgType: msgType };
{:copy-code}
```
<ul>
<li>
Generate message of type <code>POST_TELEMETRY_REQUEST</code> with <code>temp</code> value <code>42</code>,
<code>humidity</code> value <code>77</code><br>
and <strong>metadata</strong> with field <code>data</code> having value <code>40</code>:
</li>
</ul>
```javascript
var msg = { temp: 42, humidity: 77 };
var metadata = { data: 40 };
var msgType = "POST_TELEMETRY_REQUEST";
return { msg: msg, metadata: metadata, msgType: msgType };
{:copy-code}
```
<ul>
<li>
Generate message of type <code>POST_TELEMETRY_REQUEST</code> with <code>temperature</code> value<br>
increasing and decreasing linearly in the range from <code>18</code> to <code>32</code>:
</li>
</ul>
```javascript
var lower = 18;
var upper = 32;
var isDecrement = 'false';
var temperature = lower;
// Get previous values
if (typeof prevMetadata !== 'undefined' &&
typeof prevMetadata.isDecrement !== 'undefined') {
isDecrement = prevMetadata.isDecrement;
}
if (typeof prevMsg !== 'undefined' &&
typeof prevMsg.temperature !== 'undefined') {
temperature = prevMsg.temperature;
}
if (isDecrement === 'true') {
temperature--;
if (temperature <= lower) {
isDecrement = 'false';
temperature = lower;
}
} else {
temperature++;
if (temperature >= upper) {
isDecrement = 'true';
temperature = upper;
}
}
var msg = { temperature: temperature };
var metadata = { isDecrement: isDecrement };
var msgType = "POST_TELEMETRY_REQUEST";
return { msg: msg, metadata: metadata, msgType: msgType };
{:copy-code}
```
<br>
<br>

37
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/log_node_script_fn.md

@ -0,0 +1,37 @@
#### Message to string function
<div class="divider"></div>
<br/>
*function toString(msg, metadata, msgType): string*
JavaScript function transforming incoming Message to String for further logging to the server log file.
**Parameters:**
{% include rulenode/common_node_script_args %}
**Returns:**
Should return `string` value used for logging to the server log file.
<div class="divider"></div>
##### Examples
* Create string message containing incoming message and incoming metadata values:
```javascript
return 'Incoming message:\n' + JSON.stringify(msg) +
'\nIncoming metadata:\n' + JSON.stringify(metadata);
{:copy-code}
```
<br>
You can see real life example, how to use this node in this tutorial:
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#log-unknown-request)
<br>
<br>

96
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/switch_node_script_fn.md

@ -0,0 +1,96 @@
#### Switch message function
<div class="divider"></div>
<br/>
*function Switch(msg, metadata, msgType): string[]*
JavaScript function computing **an array of next Relation names** for incoming Message.
**Parameters:**
{% include rulenode/common_node_script_args %}
**Returns:**
Should return an array of `string` values presenting **next Relation names** where Message should be routed.<br>
If returned array is empty - message will not be routed to any Node and discarded.
<div class="divider"></div>
##### Examples
<ul>
<li>
Forward all messages with <code>temperature</code> value greater than <code>30</code> to the <strong>'High temperature'</strong> chain,<br>
with <code>temperature</code> value lower than <code>20</code> to the <strong>'Low temperature'</strong> chain and all other messages<br>
to the <strong>'Normal temperature'</strong> chain:
</li>
</ul>
```javascript
if (msg.temperature > 30) {
return ['High temperature'];
} else if (msg.temperature < 20) {
return ['Low temperature'];
} else {
return ['Normal temperature'];
}
{:copy-code}
```
<ul>
<li>
For messages with type <code>POST_TELEMETRY_REQUEST</code>:
<ul>
<li>
if <code>temperature</code> value lower than <code>18</code> forward to the <strong>'Low temperature telemetry'</strong> chain,
</li>
<li>
otherwise to the <strong>'Normal temperature telemetry'</strong> chain.
</li>
</ul>
For messages with type <code>POST_ATTRIBUTES_REQUEST</code>:<br>
<ul>
<li>
if <code>currentState</code> value is <code>IDLE</code> forward to the <strong>'Idle State'</strong> and <strong>'Update State Attribute'</strong> chains,
</li>
<li>
if <code>currentState</code> value is <code>RUNNING</code> forward to the <strong>'Running State'</strong> and <strong>'Update State Attribute'</strong> chains,
</li>
<li>
otherwise to the <strong>'Unknown State'</strong> chain.
</li>
</ul>
For all other message types - discard the message (do not route to any Node).
</li>
</ul>
```javascript
if (msgType === 'POST_TELEMETRY_REQUEST') {
if (msg.temperature < 18) {
return ['Low Temperature Telemetry'];
} else {
return ['Normal Temperature Telemetry'];
}
} else if (msgType === 'POST_ATTRIBUTES_REQUEST') {
if (msg.currentState === 'IDLE') {
return ['Idle State', 'Update State Attribute'];
} else if (msg.currentState === 'RUNNING') {
return ['Running State', 'Update State Attribute'];
} else {
return ['Unknown State'];
}
}
return [];
{:copy-code}
```
<br>
You can see real life example, how to use this node in this tutorial:
- [Data function based on telemetry from 2 devices{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/function-based-on-telemetry-from-two-devices#delta-temperature-rule-chain)
<br>
<br>

22
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/transformation_node_script_fn.md

@ -3,20 +3,13 @@
<div class="divider"></div>
<br/>
*function (msg, metadata msgType): {msg: object, metadata: object, msgType: string}*
*function Transform(msg, metadata, msgType): {msg: object, metadata: object, msgType: string}*
JavaScript function transforming input Message payload, Metadata or Message type.
**Parameters:**
<ul>
<li><b>msg:</b> <code>{[key: string]: any}</code> - is a Message payload key/value object.
</li>
<li><b>metadata:</b> <code>{[key: string]: string}</code> - is a Message metadata key/value object.
</li>
<li><b>msgType:</b> <code>string</code> - is a string Message type. See <a href="https://github.com/thingsboard/thingsboard/blob/ea039008b148453dfa166cf92bc40b26e487e660/ui-ngx/src/app/shared/models/rule-node.models.ts#L338" target="_blank">MessageType</a> enum for common used values.
</li>
</ul>
{% include rulenode/common_node_script_args %}
**Returns:**
@ -47,8 +40,6 @@ return { msgType: 'CUSTOM_REQUEST' };
<li>Change message type to <code>CUSTOM_UPDATE</code>,<br/>add additional attribute <strong><em>version</em></strong> into payload with value <strong><em>v1.1</em></strong>,<br/>change <strong><em>sensorType</em></strong> attribute value in Metadata to <strong><em>roomTemp</em></strong>:</li>
</ul>
The following transform function will perform all necessary modifications:
```javascript
var newType = "CUSTOM_UPDATE";
msg.version = "v1.1";
@ -57,9 +48,12 @@ return {msg: msg, metadata: metadata, msgType: newType};
{:copy-code}
```
You can see real life example, how to use this node in those tutorials:
<br>
- [Transform incoming telemetry{:target="_blank"}](https://thingsboard.io/docs/user-guide/rule-engine-2-0/tutorials/transform-incoming-telemetry/)
- [Reply to RPC Calls{:target="_blank"}](https://thingsboard.io/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-transform-script-node)
You can see real life example, how to use this node in those tutorials:
- [Transform incoming telemetry{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/transform-incoming-telemetry/)
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-transform-script-node)
<br>
<br>

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

File diff suppressed because one or more lines are too long

1
ui-ngx/angular.json

@ -97,6 +97,7 @@
"node_modules/prismjs/components/prism-bash.min.js",
"node_modules/prismjs/components/prism-json.min.js",
"node_modules/prismjs/components/prism-javascript.min.js",
"node_modules/prismjs/components/prism-typescript.min.js",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js"
],
"customWebpackConfig": {

2
ui-ngx/package.json

@ -73,7 +73,7 @@
"ngx-drag-drop": "^2.0.0",
"ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
"ngx-hm-carousel": "^2.0.0-rc.1",
"ngx-markdown": "^10.1.1",
"ngx-markdown": "^11.1.3",
"ngx-sharebuttons": "^8.0.5",
"ngx-translate-messageformat-compiler": "^4.9.0",
"objectpath": "^2.0.0",

66
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -23,10 +23,12 @@ import {
NgModule,
NgModuleRef,
OnDestroy,
Type
Type,
ɵresetCompiledComponents
} from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import { from, Observable } from 'rxjs';
import { CommonModule } from '@angular/common';
import { catchError, map, mergeMap } from 'rxjs/operators';
@NgModule()
export abstract class DynamicComponentModule implements OnDestroy {
@ -55,11 +57,12 @@ export class DynamicComponentFactoryService {
public createDynamicComponentFactory<T>(
componentType: Type<T>,
template: string,
modules?: Type<any>[]): Observable<ComponentFactory<T>> {
const dymamicComponentFactorySubject = new ReplaySubject<ComponentFactory<T>>();
import('@angular/compiler').then(
() => {
const comp = this.createDynamicComponent(componentType, template);
modules?: Type<any>[],
preserveWhitespaces?: boolean,
compileAttempt = 1): Observable<ComponentFactory<T>> {
return from(import('@angular/compiler')).pipe(
mergeMap(() => {
const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces);
let moduleImports: Type<any>[] = [CommonModule];
if (modules) {
moduleImports = [...moduleImports, ...modules];
@ -69,29 +72,33 @@ export class DynamicComponentFactoryService {
declarations: [comp],
imports: moduleImports
})(class DynamicComponentInstanceModule extends DynamicComponentModule {});
try {
this.compiler.compileModuleAsync(dynamicComponentInstanceModule).then(
(module) => {
const moduleRef = module.create(this.injector);
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp);
this.dynamicComponentModulesMap.set(factory, {
moduleRef,
moduleType: module.moduleType
});
dymamicComponentFactorySubject.next(factory);
dymamicComponentFactorySubject.complete();
return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe(
map((module) => {
let moduleRef: NgModuleRef<any>;
try {
moduleRef = module.create(this.injector);
} catch (e) {
this.compiler.clearCacheFor(module.moduleType);
throw e;
}
).catch(
(e) => {
dymamicComponentFactorySubject.error(e);
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp);
this.dynamicComponentModulesMap.set(factory, {
moduleRef,
moduleType: module.moduleType
});
return factory;
}),
catchError((error) => {
if (compileAttempt === 1) {
ɵresetCompiledComponents();
return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt);
} else {
throw error;
}
);
} catch (e) {
dymamicComponentFactorySubject.error(e);
}
}
})
);
})
);
return dymamicComponentFactorySubject.asObservable();
}
public destroyDynamicComponentFactory<T>(factory: ComponentFactory<T>) {
@ -103,10 +110,11 @@ export class DynamicComponentFactoryService {
}
}
private createDynamicComponent<T>(componentType: Type<T>, template: string): Type<T> {
private createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean): Type<T> {
// noinspection AngularMissingOrInvalidDeclarationInModule
return Component({
template
template,
preserveWhitespaces
})(componentType);
}

29
ui-ngx/src/app/core/services/help.service.ts

@ -18,7 +18,8 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
const NOT_FOUND_CONTENT = '## Not found';
@ -27,6 +28,8 @@ const NOT_FOUND_CONTENT = '## Not found';
})
export class HelpService {
private helpBaseUrl = helpBaseUrl;
private helpCache: {[lang: string]: {[key: string]: string}} = {};
constructor(
@ -52,6 +55,9 @@ export class HelpService {
return of(NOT_FOUND_CONTENT);
}
}),
mergeMap((content) => {
return this.processIncludes(this.processVariables(content));
}),
tap((content) => {
let langContent = this.helpCache[lang];
if (!langContent) {
@ -68,4 +74,25 @@ export class HelpService {
return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} );
}
private processVariables(content: string): string {
const baseUrlReg = /\${baseUrl}/g;
return content.replace(baseUrlReg, this.helpBaseUrl);
}
private processIncludes(content: string): Observable<string> {
const includesRule = /{% include (.*) %}/;
const match = includesRule.exec(content);
if (match) {
const key = match[1];
return this.getHelpContent(key).pipe(
mergeMap((include) => {
content = content.replace(match[0], include);
return this.processIncludes(content);
})
);
} else {
return of(content);
}
}
}

3
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html

@ -46,7 +46,8 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
[disableUndefinedCheck]="true"
[validationArgs]="[]"
[editorCompleter]="customPrettyActionEditorCompleter">
[editorCompleter]="customPrettyActionEditorCompleter"
helpId="widget/action/custom_pretty_action_fn">
</tb-js-func>
</div>
</div>

3
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html

@ -96,7 +96,8 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
[disableUndefinedCheck]="true"
[validationArgs]="[]"
[editorCompleter]="customPrettyActionEditorCompleter">
[editorCompleter]="customPrettyActionEditorCompleter"
helpId="widget/action/custom_pretty_action_fn">
</tb-js-func>
</mat-tab>
</mat-tab-group>

8
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html

@ -37,6 +37,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_get_location_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.makePhoneCall">
@ -46,6 +47,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_get_phone_number_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto ||
@ -58,6 +60,7 @@
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_image_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.scanQrCode">
@ -67,6 +70,7 @@
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_qr_code_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.getLocation">
@ -76,6 +80,7 @@
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_location_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
@ -88,6 +93,7 @@
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_process_launch_result_fn"
></tb-js-func>
</ng-template>
</div>
@ -97,6 +103,7 @@
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_handle_empty_result_fn"
></tb-js-func>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleErrorFunction"
@ -104,5 +111,6 @@
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/mobile_handle_error_fn"
></tb-js-func>
</div>

6
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts

@ -111,12 +111,6 @@ const processLocationFunction =
'showLocationDialog(\'Location\', latitude, longitude);\n' +
'// saveEntityLocationAttributes(\'latitude\', \'longitude\', latitude, longitude);\n' +
'\n' +
'function showImageDialog(title, imageUrl) {\n' +
' setTimeout(function() {\n' +
' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' +
' }, 100);\n' +
'}\n' +
'\n' +
'function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {\n' +
' if (entityId) {\n' +
' let attributes = [\n' +

1
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -224,6 +224,7 @@
[globalVariables]="functionScopeVariables"
[validationArgs]="[]"
[editorCompleter]="customActionEditorCompleter"
helpId="widget/action/custom_action_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.customPretty">

2
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="dataKeyFormGroup" (ngSubmit)="save()" style="min-width: 480px;">
<form [formGroup]="dataKeyFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ 'datakey.configuration' | translate }}</h2>
<span fxFlex></span>

2
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html

@ -70,6 +70,7 @@
[globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1],[1, '1']]"
resultType="any"
helpId="widget/config/datakey_generation_fn"
formControlName="funcBody">
</tb-js-func>
</section>
@ -82,6 +83,7 @@
[globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]"
resultType="any"
helpId="widget/config/datakey_postprocess_fn"
formControlName="postFuncBody">
</tb-js-func>
<label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;">

2
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html

@ -15,4 +15,4 @@
limitations under the License.
-->
<markdown [data]="markdownText" lineNumbers class="tb-markdown-view" (click)="markdownClick($event)"></markdown>
<tb-markdown [data]="markdownText" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown>

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

@ -107,9 +107,9 @@ import { ComponentType } from '@angular/cdk/portal';
import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
import { MobileService } from '@core/services/mobile.service';
import { DialogService } from '@core/services/dialog.service';
import { TbPopoverService } from '@shared/components/popover.component';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { PopoverPlacement } from '@shared/components/popover.models';
import { TbPopoverService } from '@shared/components/popover.service';
@Component({
selector: 'tb-widget',

1
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -251,6 +251,7 @@
<button mat-button (click)="beautifyJs()">
{{ 'widget.tidy' | translate }}
</button>
<span tb-help-popup="widget/editor/widget_js_fn" tb-help-popup-placement="top" [tb-help-popup-style]="{maxWidth: '1200px'}"></span>
<button mat-icon-button class="tb-mat-32"
(click)="javascriptFullscreen = !javascriptFullscreen"
matTooltip="{{(javascriptFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"

18
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -117,14 +117,17 @@ div.tb-editor-area-title-panel {
font-size: .8rem;
font-weight: 500;
& > * {
&:not(:last-child) {
margin-right: 4px;
}
}
label {
padding: 4px;
color: #00acc1;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
&:not(:last-child) {
margin-right: 4px;
}
}
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
@ -133,15 +136,16 @@ div.tb-editor-area-title-panel {
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
&:not(.tb-help-popup-button) {
color: #7b7b7b;
}
}
.tb-help-popup-button-loading {
background: #f3f3f3;
}
}
.tb-resize-container {

4
ui-ngx/src/app/shared/components/help-markdown.component.html

@ -15,6 +15,4 @@
limitations under the License.
-->
<ng-container *ngIf="markdownText$ | async as text;">
<markdown [data]="text" lineNumbers (ready)="onMarkdownReady()" class="tb-help-markdown tb-markdown-view" (click)="markdownClick($event)"></markdown>
</ng-container>
<tb-markdown [style]="style" [data]="markdownText$ | async" lineNumbers (ready)="onMarkdownReady()" markdownClass="tb-help-markdown" (click)="markdownClick($event)"></tb-markdown>

6
ui-ngx/src/app/shared/components/help-markdown.component.scss

@ -13,16 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
:host ::ng-deep {
.tb-help-markdown {
overflow: auto;
max-width: 80vw;
max-height: 80vh;
margin-top: 30px;
}
}
:host ::ng-deep {
.tb-help-markdown.tb-markdown-view {
h1, h2, h3, h4, h5, h6 {
&:first-child {

14
ui-ngx/src/app/shared/components/help-markdown.component.ts

@ -22,7 +22,7 @@ import {
Output, SimpleChanges
} from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { delay, share } from 'rxjs/operators';
import { share } from 'rxjs/operators';
import { HelpService } from '@core/services/help.service';
@Component({
@ -34,8 +34,12 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
@Input() helpId: string;
@Input() helpContent: string;
@Input() visible: boolean;
@Input() style: { [klass: string]: any } = {};
@Output() markdownReady = new EventEmitter<void>();
markdownText = new BehaviorSubject<string>(null);
@ -44,8 +48,6 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
share()
);
isMarkdownReady = false;
private loadHelpPending = false;
constructor(private help: HelpService) {}
@ -68,7 +70,7 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
this.loadHelp();
}
}
if (propName === 'helpId') {
if (propName === 'helpId' || propName === 'helpContent') {
this.markdownText.next(null);
this.loadHelpWhenVisible();
}
@ -89,16 +91,16 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
this.help.getHelpContent(this.helpId).subscribe((content) => {
this.markdownText.next(content);
});
} else if (this.helpContent) {
this.markdownText.next(this.helpContent);
}
}
onMarkdownReady() {
this.isMarkdownReady = true;
this.markdownReady.next();
}
markdownClick($event: MouseEvent) {
}
}

38
ui-ngx/src/app/shared/components/help-popup.component.html

@ -15,16 +15,28 @@
limitations under the License.
-->
<div style="position: relative;">
<button color="primary" mat-button mat-icon-button
class="tb-help-popup-button tb-mat-32"
type="button"
(click)="toggleHelp()"
matTooltip="{{'help.show-help' | translate}}"
matTooltipPosition="above">
<mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon>
</button>
<div *ngIf="popoverVisible && !popoverReady" fxFlex fxLayoutAlign="center center" class="tb-absolute-fill tb-help-popup-button-loading">
<mat-spinner mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner>
</div>
</div>
<button #toggleHelpButton
*ngIf="!textMode"
mat-icon-button
color="primary"
class="tb-help-popup-button tb-mat-32"
type="button"
(click)="toggleHelp()"
matTooltip="{{'help.show-help' | translate}}"
matTooltipPosition="above">
<mat-icon class="material-icons">{{popoverVisible ? 'help' : 'help_outline'}}</mat-icon>
<mat-spinner *ngIf="popoverVisible && !popoverReady" class="tb-help-popup-button-loading" mode="indeterminate" diameter="20" strokeWidth="2"></mat-spinner>
</button>
<button #toggleHelpTextButton
*ngIf="textMode"
mat-button
color="primary"
class="tb-help-popup-text-button"
[ngClass]="{'mat-stroked-button': popoverVisible && popoverReady}"
(click)="toggleHelp()">
<ng-container *ngIf="triggerSafeHtml">
<span [style]="triggerStyle" [innerHTML]="triggerSafeHtml"></span>
</ng-container>
<mat-icon *ngIf="!popoverVisible || popoverReady" class="tb-mat-16">open_in_new</mat-icon>
<mat-spinner *ngIf="popoverVisible && !popoverReady" mode="indeterminate" diameter="16" strokeWidth="2"></mat-spinner>
</button>

34
ui-ngx/src/app/shared/components/help-popup.component.scss

@ -14,8 +14,36 @@
* limitations under the License.
*/
.tb-help-popup-button {
position: relative;
.mat-progress-spinner {
position: absolute;
top: 0;
left: 0;
background: #fff;
border-radius: 50%;
width: 32px !important;
height: 32px !important;
svg {
top: 6px;
left: 6px;
}
}
}
.tb-help-popup-button-loading {
background: #fff;
border-radius: 50%;
.tb-help-popup-text-button {
position: relative;
padding: 0 2px 0 8px;
line-height: 28px;
&.mat-stroked-button {
padding: 0 1px 0 7px;
line-height: 26px;
}
.mat-icon {
padding-left: 4px;
}
.mat-progress-spinner {
display: inline-block;
margin-left: 4px;
margin-right: 5px;
}
}

62
ui-ngx/src/app/shared/components/help-popup.component.ts

@ -14,8 +14,20 @@
/// limitations under the License.
///
import { Component, ElementRef, Input, OnDestroy, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { TbPopoverService } from '@shared/components/popover.component';
import {
Component,
ElementRef,
Input, OnChanges,
OnDestroy,
Renderer2, SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { TbPopoverService } from '@shared/components/popover.service';
import { PopoverPlacement } from '@shared/components/popover.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
// tslint:disable-next-line:component-selector
@ -24,27 +36,61 @@ import { TbPopoverService } from '@shared/components/popover.component';
styleUrls: ['./help-popup.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class HelpPopupComponent implements OnDestroy {
export class HelpPopupComponent implements OnChanges, OnDestroy {
@ViewChild('toggleHelpButton', {read: ElementRef, static: false}) toggleHelpButton: ElementRef;
@ViewChild('toggleHelpTextButton', {read: ElementRef, static: false}) toggleHelpTextButton: ElementRef;
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup') helpId: string;
// tslint:disable-next-line:no-input-rename
@Input('trigger-text') triggerText: string;
// tslint:disable-next-line:no-input-rename
@Input('trigger-style') triggerStyle: string;
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup-placement') helpPopupPlacement: PopoverPlacement;
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup-style') helpPopupStyle: { [klass: string]: any } = {};
popoverVisible = false;
popoverReady = true;
constructor(private elementRef: ElementRef,
private viewContainerRef: ViewContainerRef,
triggerSafeHtml: SafeHtml = null;
textMode = false;
constructor(private viewContainerRef: ViewContainerRef,
private element: ElementRef<HTMLElement>,
private sanitizer: DomSanitizer,
private renderer: Renderer2,
private popoverService: TbPopoverService) {}
private popoverService: TbPopoverService) {
}
ngOnChanges(changes: SimpleChanges): void {
if (isDefinedAndNotNull(this.triggerText)) {
this.triggerSafeHtml = this.sanitizer.bypassSecurityTrustHtml(this.triggerText);
} else {
this.triggerSafeHtml = null;
}
this.textMode = this.triggerSafeHtml != null;
}
toggleHelp() {
this.popoverService.toggleHelpPopover(this.elementRef.nativeElement, this.renderer, this.viewContainerRef,
const trigger = this.textMode ? this.toggleHelpTextButton.nativeElement : this.toggleHelpButton.nativeElement;
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef,
this.helpId,
'',
(visible) => {
this.popoverVisible = visible;
}, (ready => {
this.popoverReady = ready;
}));
}),
this.helpPopupPlacement,
{},
this.helpPopupStyle);
}
ngOnDestroy(): void {

5
ui-ngx/src/app/shared/components/json-form/json-form.component.ts

@ -46,8 +46,7 @@ import { GroupInfo } from '@shared/models/widget.models';
import { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from } from 'rxjs';
import { MouseEvent } from 'react';
import { TbPopoverService } from '@shared/components/popover.component';
import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
import { TbPopoverService } from '@shared/components/popover.service';
const tinycolor = tinycolor_;
@ -252,7 +251,7 @@ export class JsonFormComponent implements OnInit, ControlValueAccessor, Validato
private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) {
const trigger = event.currentTarget as Element;
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, helpVisibleFn, helpReadyFn);
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, '', helpVisibleFn, helpReadyFn);
}
private updateAndRender() {

26
ui-ngx/src/app/shared/components/markdown.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container #markdownContainer>
</ng-container>
<div *ngIf="error && !fallbackToPlainMarkdown" style="color: #f00; font-size: 14px;
line-height: 28px;
background: #efefef;">
{{error}}
</div>
<div #fallbackElement [fxShow]="error && fallbackToPlainMarkdown" class="tb-markdown-view" [ngClass]="markdownClass" [ngStyle]="style">
</div>

194
ui-ngx/src/app/shared/components/markdown.component.ts

@ -0,0 +1,194 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
ComponentFactory,
ComponentRef, ElementRef,
EventEmitter,
Inject,
Injector,
Input, OnChanges,
Output,
SimpleChanges,
Type, ViewChild,
ViewContainerRef
} from '@angular/core';
import { HelpService } from '@core/services/help.service';
import { MarkdownService, PrismPlugin } from 'ngx-markdown';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { isDefinedAndNotNull } from '@core/utils';
import { Observable, of, ReplaySubject } from 'rxjs';
@Component({
selector: 'tb-markdown',
templateUrl: './markdown.component.html'
})
export class TbMarkdownComponent implements OnChanges {
@ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef;
@ViewChild('fallbackElement', {static: true}) fallbackElement: ElementRef<HTMLElement>;
@Input() data: string | undefined;
@Input() markdownClass: string | undefined;
@Input() style: { [klass: string]: any } = {};
@Input()
get lineNumbers(): boolean { return this.lineNumbersValue; }
set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); }
@Input()
get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; }
set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); }
@Output() ready = new EventEmitter<void>();
private lineNumbersValue = false;
private fallbackToPlainMarkdownValue = false;
isMarkdownReady = false;
error = null;
private tbMarkdownInstanceComponentRef: ComponentRef<any>;
private tbMarkdownInstanceComponentFactory: ComponentFactory<any>;
constructor(private help: HelpService,
private cd: ChangeDetectorRef,
public markdownService: MarkdownService,
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
private dynamicComponentFactoryService: DynamicComponentFactoryService) {}
ngOnChanges(changes: SimpleChanges): void {
if (isDefinedAndNotNull(this.data)) {
this.render(this.data);
}
}
private render(markdown: string) {
const compiled = this.markdownService.compile(markdown, false);
let template = this.sanitizeCurlyBraces(compiled);
let markdownClass = 'tb-markdown-view';
if (this.markdownClass) {
markdownClass += ` ${this.markdownClass}`;
}
template = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`;
this.markdownContainer.clear();
const parent = this;
let readyObservable: Observable<void>;
this.dynamicComponentFactoryService.createDynamicComponentFactory(
class TbMarkdownInstance {
ngOnDestroy(): void {
parent.destroyMarkdownInstanceResources();
}
},
template,
[this.sharedModule],
true
).subscribe((factory) => {
this.tbMarkdownInstanceComponentFactory = factory;
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector});
try {
this.tbMarkdownInstanceComponentRef =
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentFactory, 0, injector);
this.tbMarkdownInstanceComponentRef.instance.style = this.style;
this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement);
this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement);
readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement);
this.error = null;
} catch (error) {
readyObservable = this.handleError(compiled, error);
}
this.cd.detectChanges();
readyObservable.subscribe(() => {
this.ready.emit();
});
},
(error) => {
readyObservable = this.handleError(compiled, error);
this.cd.detectChanges();
readyObservable.subscribe(() => {
this.ready.emit();
});
});
}
private handleError(template: string, error): Observable<void> {
this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '<br>');
this.destroyMarkdownInstanceResources();
if (this.fallbackToPlainMarkdownValue) {
this.markdownContainer.clear();
const element = this.fallbackElement.nativeElement;
element.innerHTML = template;
this.handlePlugins(element);
this.markdownService.highlight(element);
return this.handleImages(element);
} else {
return of(null);
}
}
private handlePlugins(element: HTMLElement): void {
if (this.lineNumbers) {
this.setPluginClass(element, PrismPlugin.LineNumbers);
}
}
private setPluginClass(element: HTMLElement, plugin: string | string[]): void {
const preElements = element.querySelectorAll('pre');
for (let i = 0; i < preElements.length; i++) {
const classes = plugin instanceof Array ? plugin : [plugin];
preElements.item(i).classList.add(...classes);
}
}
private handleImages(element: HTMLElement): Observable<void> {
const imgs = $('img', element);
if (imgs.length) {
let totalImages = imgs.length;
const imagesLoadedSubject = new ReplaySubject<void>();
imgs.each((index, img) => {
$(img).one('load error', () => {
totalImages--;
if (totalImages === 0) {
imagesLoadedSubject.next();
imagesLoadedSubject.complete();
}
});
});
return imagesLoadedSubject.asObservable();
} else {
return of(null);
}
}
private sanitizeCurlyBraces(template: string): string {
return template.replace(/{/g, '&#123;').replace(/}/g, '&#125;');
}
private destroyMarkdownInstanceResources() {
if (this.tbMarkdownInstanceComponentFactory) {
this.dynamicComponentFactoryService.destroyDynamicComponentFactory(this.tbMarkdownInstanceComponentFactory);
this.tbMarkdownInstanceComponentFactory = null;
}
this.tbMarkdownInstanceComponentRef = null;
}
}

30
ui-ngx/src/app/shared/components/marked-options.service.ts

@ -21,6 +21,7 @@ import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
const copyCodeBlock = '{:copy-code}';
const autoBlock = '{:auto}';
const targetBlankBlock = '{:target=&quot;_blank&quot;}';
// @dynamic
@ -48,13 +49,25 @@ export class MarkedOptionsService extends MarkedOptions {
this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
if (code.endsWith(copyCodeBlock)) {
code = code.substring(0, code.length - copyCodeBlock.length);
const content = checkLineNumbers(this.renderer2.code(code, language, isEscaped), code);
const content = postProcessCodeContent(this.renderer2.code(code, language, isEscaped), code);
this.id++;
return this.wrapCopyCode(this.id, content, code);
} else {
return this.wrapDiv(checkLineNumbers(this.renderer2.code(code, language, isEscaped), code));
return this.wrapDiv(postProcessCodeContent(this.renderer2.code(code, language, isEscaped), code));
}
};
this.renderer.table = (header: string, body: string) => {
let autoLayout = false;
if (header.includes(autoBlock)) {
autoLayout = true;
header = header.replace(autoBlock, '');
}
let table = this.renderer2.table(header, body);
if (autoLayout) {
table = table.replace('<table', '<table class="auto"');
}
return table;
};
this.renderer.tablecell = (content: string, flags: {
header: boolean;
align: 'center' | 'left' | 'right' | null;
@ -85,7 +98,7 @@ export class MarkedOptionsService extends MarkedOptions {
private wrapCopyCode(id: number, content: string, code: string): string {
return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` +
`<span id="copyCodeId${id}" style="display: none;">${code}</span>` +
`<span id="copyCodeId${id}" style="display: none;">${encodeURIComponent(code)}</span>` +
`<button class="clipboard-btn">\n` +
` <p>${this.translate.instant('markdown.copy-code')}</p>\n` +
` <div>\n` +
@ -119,7 +132,7 @@ export class MarkedOptionsService extends MarkedOptions {
private markdownCopyCode(id: number) {
const copyWrapper = $('#codeWrapper' + id);
if (copyWrapper.hasClass('noChars')) {
const text = $('#copyCodeId' + id).text();
const text = decodeURIComponent($('#copyCodeId' + id).text());
this.window.navigator.clipboard.writeText(text).then(() => {
import('tooltipster').then(
() => {
@ -151,10 +164,13 @@ export class MarkedOptionsService extends MarkedOptions {
}
}
function checkLineNumbers(content: string, code: string): string {
function postProcessCodeContent(content: string, code: string): string {
const lineCount = code.trim().split('\n').length;
let replacement;
if (lineCount < 2) {
content = content.replace('<pre>', '<pre class="no-line-numbers">');
replacement = '<pre ngNonBindable class="no-line-numbers">';
} else {
replacement = '<pre ngNonBindable>';
}
return content;
return content.replace('<pre>', replacement);
}

164
ui-ngx/src/app/shared/components/popover.component.ts

@ -25,7 +25,6 @@ import {
Directive,
ElementRef,
EventEmitter,
Injectable,
Injector,
Input,
OnChanges,
@ -36,7 +35,6 @@ import {
Renderer2,
SimpleChanges,
TemplateRef,
Type,
ViewChild,
ViewContainerRef,
ViewEncapsulation
@ -54,13 +52,11 @@ import {
getPlacementName,
popoverMotion,
PopoverPlacement,
PopoverWithTrigger,
POSITION_MAP,
PropertyMapping
} from '@shared/components/popover.models';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { isNotEmptyStr, onParentScrollOrWindowResize } from '@core/utils';
import { HelpMarkdownComponent } from '@shared/components/help-markdown.component';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@ -285,162 +281,6 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
}
}
@Injectable()
export class TbPopoverService {
private popoverWithTriggers: PopoverWithTrigger[] = [];
componentFactory: ComponentFactory<TbPopoverComponent> = this.resolver.resolveComponentFactory(TbPopoverComponent);
constructor(private resolver: ComponentFactoryResolver) {
}
hasPopover(trigger: Element): boolean {
const res = this.findPopoverByTrigger(trigger);
return res !== null;
}
hidePopover(trigger: Element): boolean {
const component: TbPopoverComponent = this.findPopoverByTrigger(trigger);
if (component && component.tbVisible) {
component.hide();
return true;
} else {
return false;
}
}
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any): TbPopoverComponent {
const componentRef = hostView.createComponent(this.componentFactory);
const component = componentRef.instance;
this.popoverWithTriggers.push({
trigger,
popoverComponent: component
});
renderer.removeChild(
renderer.parentNode(trigger),
componentRef.location.nativeElement
);
const originElementRef = new ElementRef(trigger);
component.setOverlayOrigin({ elementRef: originElementRef });
component.tbPlacement = preferredPlacement;
component.tbComponentFactory = this.resolver.resolveComponentFactory(componentType);
component.tbComponentInjector = injector;
component.tbComponentContext = context;
component.tbOverlayStyle = overlayStyle;
component.tbPopoverInnerStyle = popoverStyle;
component.tbComponentStyle = style;
component.tbHideOnClickOutside = hideOnClickOutside;
component.tbVisibleChange.subscribe((visible: boolean) => {
if (!visible) {
component.tbAnimationDone.subscribe(() => {
componentRef.destroy();
});
}
});
component.tbDestroy.subscribe(() => {
this.removePopoverByComponent(component);
});
component.show();
return component;
}
toggleHelpPopover(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, helpId: string,
visibleFn: (visible: boolean) => void, readyFn: (ready: boolean) => void) {
if (this.hasPopover(trigger)) {
this.hidePopover(trigger);
} else {
readyFn(false);
const injector = Injector.create({
parent: hostView.injector, providers: []
});
const componentRef = hostView.createComponent(this.componentFactory);
const component = componentRef.instance;
this.popoverWithTriggers.push({
trigger,
popoverComponent: component
});
renderer.removeChild(
renderer.parentNode(trigger),
componentRef.location.nativeElement
);
const originElementRef = new ElementRef(trigger);
component.tbAnimationState = 'void';
component.tbOverlayStyle = { opacity: '0' };
component.setOverlayOrigin({ elementRef: originElementRef });
component.tbPlacement = 'bottom';
component.tbComponentFactory = this.resolver.resolveComponentFactory(HelpMarkdownComponent);
component.tbComponentInjector = injector;
component.tbComponentContext = {
helpId,
visible: true
};
component.tbHideOnClickOutside = true;
component.tbVisibleChange.subscribe((visible: boolean) => {
if (!visible) {
visibleFn(false);
component.tbAnimationDone.subscribe(() => {
componentRef.destroy();
});
}
});
component.tbDestroy.subscribe(() => {
this.removePopoverByComponent(component);
});
const showHelpMarkdownComponent = () => {
component.tbOverlayStyle = { opacity: '1' };
component.tbAnimationState = 'active';
component.updatePosition();
readyFn(true);
setTimeout(() => {
component.updatePosition();
});
};
const setupHelpMarkdownComponent = (helpMarkdownComponent: HelpMarkdownComponent) => {
if (helpMarkdownComponent.isMarkdownReady) {
showHelpMarkdownComponent();
} else {
helpMarkdownComponent.markdownReady.subscribe(() => {
showHelpMarkdownComponent();
});
}
};
if (component.tbComponentRef) {
setupHelpMarkdownComponent(component.tbComponentRef.instance);
} else {
component.tbComponentChange.subscribe((helpMarkdownComponentRef) => {
setupHelpMarkdownComponent(helpMarkdownComponentRef.instance);
});
}
component.show();
visibleFn(true);
}
}
private findPopoverByTrigger(trigger: Element): TbPopoverComponent | null {
const res = this.popoverWithTriggers.find(val => this.elementsAreEqualOrDescendant(trigger, val.trigger));
if (res) {
return res.popoverComponent;
} else {
return null;
}
}
private removePopoverByComponent(component: TbPopoverComponent): void {
const index = this.popoverWithTriggers.findIndex(val => val.popoverComponent === component);
if (index > -1) {
this.popoverWithTriggers.splice(index, 1);
}
}
private elementsAreEqualOrDescendant(element1: Element, element2: Element): boolean {
return element1 === element2 || element1.contains(element2) || element2.contains(element1);
}
}
@Component({
selector: 'tb-popover',
exportAs: 'tbPopoverComponent',
@ -703,10 +543,12 @@ export class TbPopoverComponent implements OnDestroy, OnInit {
updateStyles(): void {
this.classMap = {
[this.tbOverlayClassName]: true,
[`tb-popover-placement-${this.preferredPlacement}`]: true,
['tb-popover-hidden']: this.tbHidden || !this.lastIsIntersecting
};
if (this.tbOverlayClassName) {
this.classMap[this.tbOverlayClassName] = true;
}
}
setOverlayOrigin(origin: CdkOverlayOrigin): void {

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

Loading…
Cancel
Save