Browse Source

Merge branch 'develop/3.4' into vc-fixes

pull/6814/head
Viacheslav Klimov 4 years ago
parent
commit
bcdfab68ea
  1. 73
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  2. 5
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  3. 9
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
  4. 11
      application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListBranchesGitRequest.java
  7. 7
      common/cluster-api/src/main/proto/queue.proto
  8. 39
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/BranchInfo.java
  9. 6
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultClusterVersionControlService.java
  10. 7
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  11. 31
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  12. 5
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java
  13. 18
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  14. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
  15. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java
  16. 3
      dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java
  17. 18
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  18. 134
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  19. 156
      dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java
  20. 3
      docker/.env
  21. 7
      docker/README.md
  22. 5
      docker/cache-redis-cluster.env
  23. 2
      docker/cache-redis.env
  24. 36
      docker/compose-utils.sh
  25. 32
      docker/docker-compose.aws-sqs.yml
  26. 8
      docker/docker-compose.confluent.yml
  27. 4
      docker/docker-compose.hybrid.yml
  28. 4
      docker/docker-compose.kafka.yml
  29. 61
      docker/docker-compose.postgres.volumes.yml
  30. 8
      docker/docker-compose.postgres.yml
  31. 58
      docker/docker-compose.redis-cluster.volumes.yml
  32. 148
      docker/docker-compose.redis-cluster.yml
  33. 27
      docker/docker-compose.redis.volumes.yml
  34. 97
      docker/docker-compose.redis.yml
  35. 81
      docker/docker-compose.volumes.yml
  36. 14
      docker/docker-compose.yml
  37. 21
      docker/docker-create-log-folders.sh
  38. 6
      docker/docker-install-tb.sh
  39. 4
      docker/docker-remove-services.sh
  40. 4
      docker/docker-start-services.sh
  41. 4
      docker/docker-stop-services.sh
  42. 6
      docker/docker-update-service.sh
  43. 8
      docker/docker-upgrade-tb.sh
  44. 3
      docker/tb-coap-transport.env
  45. 3
      docker/tb-http-transport.env
  46. 3
      docker/tb-lwm2m-transport.env
  47. 3
      docker/tb-mqtt-transport.env
  48. 2
      docker/tb-node.env
  49. 3
      docker/tb-snmp-transport.env
  50. 7
      msa/black-box-tests/README.md
  51. 13
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  52. 53
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  53. 6
      msa/js-executor/api/jsInvokeMessageProcessor.js
  54. 12
      ui-ngx/src/app/core/http/entities-version-control.service.ts
  55. 2
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html
  56. 4
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html
  57. 4
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss
  58. 888
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  59. 55
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.scss
  60. 4
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  61. 274
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.html
  62. 32
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss
  63. 185
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -39,6 +38,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo; import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
@ -56,6 +56,7 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
@ -228,10 +229,10 @@ public class EntitiesVersionControlController extends BaseController {
"}" + "}" +
MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{branch}/{entityType}/{externalEntityUuid}", params = {"pageSize", "page"}) @GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"})
public DeferredResult<PageData<EntityVersion>> listEntityVersions(@PathVariable String branch, public DeferredResult<PageData<EntityVersion>> listEntityVersions(@PathVariable EntityType entityType,
@PathVariable EntityType entityType,
@PathVariable UUID externalEntityUuid, @PathVariable UUID externalEntityUuid,
@RequestParam String branch,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -254,9 +255,9 @@ public class EntitiesVersionControlController extends BaseController {
"If specified branch does not exist - empty page data will be returned. " + "If specified branch does not exist - empty page data will be returned. " +
"The response structure is the same as for `listEntityVersions` API method." + "The response structure is the same as for `listEntityVersions` API method." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{branch}/{entityType}", params = {"pageSize", "page"}) @GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"})
public DeferredResult<PageData<EntityVersion>> listEntityTypeVersions(@PathVariable String branch, public DeferredResult<PageData<EntityVersion>> listEntityTypeVersions(@PathVariable EntityType entityType,
@PathVariable EntityType entityType, @RequestParam String branch,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -277,8 +278,8 @@ public class EntitiesVersionControlController extends BaseController {
"If specified branch does not exist - empty page data will be returned. " + "If specified branch does not exist - empty page data will be returned. " +
"The response format is the same as for `listEntityVersions` API method." + "The response format is the same as for `listEntityVersions` API method." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{branch}", params = {"pageSize", "page"}) @GetMapping(value = "/version", params = {"branch", "pageSize", "page"})
public DeferredResult<PageData<EntityVersion>> listVersions(@PathVariable String branch, public DeferredResult<PageData<EntityVersion>> listVersions(@RequestParam String branch,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -300,11 +301,11 @@ public class EntitiesVersionControlController extends BaseController {
"Each entity item in the result has `externalId` property. " + "Each entity item in the result has `externalId` property. " +
"Entities order will be the same as in the repository." + "Entities order will be the same as in the repository." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/entity/{branch}/{entityType}/{versionId}") @GetMapping(value = "/entity/{entityType}/{versionId}", params = {"branch"})
public DeferredResult<List<VersionedEntityInfo>> listEntitiesAtVersion(@PathVariable String branch, public DeferredResult<List<VersionedEntityInfo>> listEntitiesAtVersion(@PathVariable EntityType entityType,
@PathVariable EntityType entityType,
@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) @ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true)
@PathVariable String versionId) throws Exception { @PathVariable String versionId,
@RequestParam String branch) throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType)); return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType));
} }
@ -314,10 +315,10 @@ public class EntitiesVersionControlController extends BaseController {
"Response type is the same as for listAllEntitiesAtVersion API method. \n" + "Response type is the same as for listAllEntitiesAtVersion API method. \n" +
"Returned entities order will be the same as in the repository." + "Returned entities order will be the same as in the repository." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/entity/{branch}/{versionId}") @GetMapping(value = "/entity/{versionId}", params = {"branch"})
public DeferredResult<List<VersionedEntityInfo>> listAllEntitiesAtVersion(@PathVariable String branch, public DeferredResult<List<VersionedEntityInfo>> listAllEntitiesAtVersion(@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true)
@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) @PathVariable String versionId,
@PathVariable String versionId) throws Exception { @RequestParam String branch) throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId)); return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId));
} }
@ -342,10 +343,10 @@ public class EntitiesVersionControlController extends BaseController {
"Returns an object with current entity data and the one at a specific version. " + "Returns an object with current entity data and the one at a specific version. " +
"Entity data structure is the same as stored in a repository. " + "Entity data structure is the same as stored in a repository. " +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}") @GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"branch", "versionId"})
public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@PathVariable String branch, public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@PathVariable EntityType entityType,
@PathVariable EntityType entityType,
@PathVariable UUID internalEntityUuid, @PathVariable UUID internalEntityUuid,
@RequestParam String branch,
@ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true) @ApiParam(value = VERSION_ID_PARAM_DESCRIPTION, required = true)
@RequestParam String versionId) throws Exception { @RequestParam String versionId) throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -485,33 +486,23 @@ public class EntitiesVersionControlController extends BaseController {
public DeferredResult<List<BranchInfo>> listBranches() throws Exception { public DeferredResult<List<BranchInfo>> listBranches() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
final TenantId tenantId = getTenantId(); final TenantId tenantId = getTenantId();
ListenableFuture<List<String>> branches = versionControlService.listBranches(tenantId); ListenableFuture<List<BranchInfo>> branches = versionControlService.listBranches(tenantId);
return wrapFuture(Futures.transform(branches, remoteBranches -> { return wrapFuture(Futures.transform(branches, remoteBranches -> {
List<BranchInfo> infos = new ArrayList<>(); List<BranchInfo> infos = new ArrayList<>();
BranchInfo defaultBranch;
String defaultBranch = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch(); String defaultBranchName = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch();
if (StringUtils.isEmpty(defaultBranch)) { if (StringUtils.isNotEmpty(defaultBranchName)) {
if (remoteBranches.contains("main")) { defaultBranch = new BranchInfo(defaultBranchName, true);
defaultBranch = "main"; } else {
} else { defaultBranch = remoteBranches.stream().filter(BranchInfo::isDefault).findFirst().orElse(null);
defaultBranch = "master";
}
} }
infos.add(new BranchInfo(defaultBranch, true)); if (defaultBranch != null) {
infos.add(defaultBranch);
for (String branch : remoteBranches) {
if (!branch.equals(defaultBranch)) {
infos.add(new BranchInfo(branch, false));
}
} }
infos.addAll(remoteBranches.stream().filter(b -> !b.equals(defaultBranch))
.map(b -> new BranchInfo(b.getName(), false)).collect(Collectors.toList()));
return infos; return infos;
}, MoreExecutors.directExecutor())); }, MoreExecutors.directExecutor()));
} }
@Data
public static class BranchInfo {
private final String name;
private final boolean isDefault;
}
} }

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

@ -25,12 +25,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
@ -49,6 +47,7 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo; import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.common.data.sync.vc.EntityLoadError; import org.thingsboard.server.common.data.sync.vc.EntityLoadError;
@ -480,7 +479,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<List<String>> listBranches(TenantId tenantId) throws Exception { public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) throws Exception {
return gitServiceQueue.listBranches(tenantId); return gitServiceQueue.listBranches(tenantId);
} }

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff; import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
@ -240,7 +241,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
} }
@Override @Override
public ListenableFuture<List<String>> listBranches(TenantId tenantId) { public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) {
ListBranchesGitRequest request = new ListBranchesGitRequest(tenantId); ListBranchesGitRequest request = new ListBranchesGitRequest(tenantId);
return sendRequest(request, builder -> builder.setListBranchesRequest(TransportProtos.ListBranchesRequestMsg.newBuilder().build())); return sendRequest(request, builder -> builder.setListBranchesRequest(TransportProtos.ListBranchesRequestMsg.newBuilder().build()));
} }
@ -382,7 +383,7 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
((CommitGitRequest) request).getFuture().set(commitResult); ((CommitGitRequest) request).getFuture().set(commitResult);
} else if (vcResponseMsg.hasListBranchesResponse()) { } else if (vcResponseMsg.hasListBranchesResponse()) {
var listBranchesResponse = vcResponseMsg.getListBranchesResponse(); var listBranchesResponse = vcResponseMsg.getListBranchesResponse();
((ListBranchesGitRequest) request).getFuture().set(listBranchesResponse.getBranchesList()); ((ListBranchesGitRequest) request).getFuture().set(listBranchesResponse.getBranchesList().stream().map(this::getBranchInfo).collect(Collectors.toList()));
} else if (vcResponseMsg.hasListEntitiesResponse()) { } else if (vcResponseMsg.hasListEntitiesResponse()) {
var listEntitiesResponse = vcResponseMsg.getListEntitiesResponse(); var listEntitiesResponse = vcResponseMsg.getListEntitiesResponse();
((ListEntitiesGitRequest) request).getFuture().set( ((ListEntitiesGitRequest) request).getFuture().set(
@ -439,6 +440,10 @@ public class DefaultGitVersionControlQueueService implements GitVersionControlQu
return new VersionedEntityInfo(EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()))); return new VersionedEntityInfo(EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())));
} }
private BranchInfo getBranchInfo(TransportProtos.BranchInfoProto proto) {
return new BranchInfo(proto.getName(), proto.getIsDefault());
}
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@SneakyThrows @SneakyThrows
private EntityExportData toData(String data) { private EntityExportData toData(String data) {

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

@ -16,22 +16,21 @@
package org.thingsboard.server.service.sync.vc; package org.thingsboard.server.service.sync.vc;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff; import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo; import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult; import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
@ -60,7 +59,7 @@ public interface EntitiesVersionControlService {
ListenableFuture<EntityDataDiff> compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception; ListenableFuture<EntityDataDiff> compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception;
ListenableFuture<List<String>> listBranches(TenantId tenantId) throws Exception; ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) throws Exception;
RepositorySettings getVersionControlSettings(TenantId tenantId); RepositorySettings getVersionControlSettings(TenantId tenantId);

7
application/src/main/java/org/thingsboard/server/service/sync/vc/GitVersionControlQueueService.java

@ -24,14 +24,15 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
import org.thingsboard.server.gen.transport.TransportProtos.VersionControlResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.VersionControlResponseMsg;
import org.thingsboard.server.service.sync.vc.data.CommitGitRequest; import org.thingsboard.server.service.sync.vc.data.CommitGitRequest;
import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff;
import java.util.List; import java.util.List;
@ -55,7 +56,7 @@ public interface GitVersionControlQueueService {
ListenableFuture<List<VersionedEntityInfo>> listEntitiesAtVersion(TenantId tenantId, String branch, String versionId); ListenableFuture<List<VersionedEntityInfo>> listEntitiesAtVersion(TenantId tenantId, String branch, String versionId);
ListenableFuture<List<String>> listBranches(TenantId tenantId); ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId);
ListenableFuture<EntityExportData> getEntity(TenantId tenantId, String versionId, EntityId entityId); ListenableFuture<EntityExportData> getEntity(TenantId tenantId, String versionId, EntityId entityId);

3
application/src/main/java/org/thingsboard/server/service/sync/vc/data/ListBranchesGitRequest.java

@ -16,10 +16,11 @@
package org.thingsboard.server.service.sync.vc.data; package org.thingsboard.server.service.sync.vc.data;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import java.util.List; import java.util.List;
public class ListBranchesGitRequest extends PendingGitRequest<List<String>> { public class ListBranchesGitRequest extends PendingGitRequest<List<BranchInfo>> {
public ListBranchesGitRequest(TenantId tenantId) { public ListBranchesGitRequest(TenantId tenantId) {
super(tenantId); super(tenantId);

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

@ -773,8 +773,13 @@ message ListEntitiesResponseMsg {
message ListBranchesRequestMsg { message ListBranchesRequestMsg {
} }
message BranchInfoProto {
string name = 1;
bool isDefault = 2;
}
message ListBranchesResponseMsg { message ListBranchesResponseMsg {
repeated string branches = 1; repeated BranchInfoProto branches = 1;
} }
message EntityContentRequestMsg { message EntityContentRequestMsg {

39
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/BranchInfo.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 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.sync.vc;
import lombok.Data;
import java.util.Objects;
@Data
public class BranchInfo {
private final String name;
private final boolean isDefault;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BranchInfo that = (BranchInfo) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}

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

@ -42,6 +42,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AddMsg; import org.thingsboard.server.gen.transport.TransportProtos.AddMsg;
import org.thingsboard.server.gen.transport.TransportProtos.BranchInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.CommitRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.CommitRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.CommitResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.CommitResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.DeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.DeleteMsg;
@ -332,7 +333,10 @@ public class DefaultClusterVersionControlService extends TbApplicationEventListe
} }
private void handleListBranches(VersionControlRequestCtx ctx, ListBranchesRequestMsg request) { private void handleListBranches(VersionControlRequestCtx ctx, ListBranchesRequestMsg request) {
var branches = vcService.listBranches(ctx.getTenantId()); var branches = vcService.listBranches(ctx.getTenantId()).stream()
.map(branchInfo -> BranchInfoProto.newBuilder()
.setName(branchInfo.getName())
.setIsDefault(branchInfo.isDefault()).build()).collect(Collectors.toList());
reply(ctx, Optional.empty(), builder -> builder.setListBranchesResponse(ListBranchesResponseMsg.newBuilder().addAllBranches(branches))); reply(ctx, Optional.empty(), builder -> builder.setListBranchesResponse(ListBranchesResponseMsg.newBuilder().addAllBranches(branches)));
} }

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

@ -29,8 +29,9 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
import org.thingsboard.server.service.sync.vc.GitRepository.Diff; import org.thingsboard.server.service.sync.vc.GitRepository.Diff;
@ -84,7 +85,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
repository.createAndCheckoutOrphanBranch(commit.getWorkingBranch()); repository.createAndCheckoutOrphanBranch(commit.getWorkingBranch());
repository.resetAndClean(); repository.resetAndClean();
if (repository.listRemoteBranches().contains(branch)) { if (repository.listRemoteBranches().contains(new BranchInfo(branch, false))) {
repository.merge(branch); repository.merge(branch);
} }
} catch (IOException | GitAPIException gitAPIException) { } catch (IOException | GitAPIException gitAPIException) {
@ -182,7 +183,7 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
} }
@Override @Override
public List<String> listBranches(TenantId tenantId) { public List<BranchInfo> listBranches(TenantId tenantId) {
GitRepository repository = checkRepository(tenantId); GitRepository repository = checkRepository(tenantId);
try { try {
return repository.listRemoteBranches(); return repository.listRemoteBranches();

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

@ -41,10 +41,12 @@ import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.SshTransport; import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
@ -58,6 +60,7 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod;
@ -69,7 +72,12 @@ import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.*; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -84,6 +92,8 @@ public class GitRepository {
@Getter @Getter
private final String directory; private final String directory;
private ObjectId headId;
private GitRepository(Git git, RepositorySettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) { private GitRepository(Git git, RepositorySettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) {
this.git = git; this.git = git;
this.settings = settings; this.settings = settings;
@ -135,8 +145,12 @@ public class GitRepository {
} }
public void fetch() throws GitAPIException { public void fetch() throws GitAPIException {
execute(git.fetch() FetchResult result = execute(git.fetch()
.setRemoveDeletedRefs(true)); .setRemoveDeletedRefs(true));
Ref head = result.getAdvertisedRef(Constants.HEAD);
if (head != null) {
this.headId = head.getObjectId();
}
} }
public void deleteLocalBranchIfExists(String branch) throws GitAPIException { public void deleteLocalBranchIfExists(String branch) throws GitAPIException {
@ -162,13 +176,11 @@ public class GitRepository {
.include(branchId)); .include(branchId));
} }
public List<BranchInfo> listRemoteBranches() throws GitAPIException {
public List<String> listRemoteBranches() throws GitAPIException {
return execute(git.branchList() return execute(git.branchList()
.setListMode(ListBranchCommand.ListMode.REMOTE)).stream() .setListMode(ListBranchCommand.ListMode.REMOTE)).stream()
.filter(ref -> !ref.getName().equals(Constants.HEAD)) .filter(ref -> !ref.getName().equals(Constants.HEAD))
.map(ref -> org.eclipse.jgit.lib.Repository.shortenRefName(ref.getName())) .map(this::toBranchInfo)
.map(name -> StringUtils.removeStart(name, "origin/"))
.distinct().collect(Collectors.toList()); .distinct().collect(Collectors.toList());
} }
@ -325,6 +337,13 @@ public class GitRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private BranchInfo toBranchInfo(Ref ref) {
String name = org.eclipse.jgit.lib.Repository.shortenRefName(ref.getName());
String branchName = StringUtils.removeStart(name, "origin/");
boolean isDefault = this.headId != null && this.headId.equals(ref.getObjectId());
return new BranchInfo(branchName, isDefault);
}
private Commit toCommit(RevCommit revCommit) { private Commit toCommit(RevCommit revCommit) {
return new Commit(revCommit.getCommitTime() * 1000l, revCommit.getName(), return new Commit(revCommit.getCommitTime() * 1000l, revCommit.getName(),
revCommit.getFullMessage(), revCommit.getAuthorIdent().getName(), revCommit.getAuthorIdent().getEmailAddress()); revCommit.getFullMessage(), revCommit.getAuthorIdent().getName(), revCommit.getAuthorIdent().getEmailAddress());

5
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepositoryService.java

@ -19,8 +19,9 @@ import org.eclipse.jgit.api.errors.GitAPIException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
import org.thingsboard.server.service.sync.vc.GitRepository.Diff; import org.thingsboard.server.service.sync.vc.GitRepository.Diff;
@ -57,7 +58,7 @@ public interface GitRepositoryService {
void abort(PendingCommit commit); void abort(PendingCommit commit);
List<String> listBranches(TenantId tenantId); List<BranchInfo> listBranches(TenantId tenantId);
String getFileContentAtCommit(TenantId tenantId, String relativePath, String versionId) throws IOException; String getFileContentAtCommit(TenantId tenantId, String relativePath, String versionId) throws IOException;

18
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java

@ -26,6 +26,7 @@ import org.springframework.data.domain.Sort;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -122,14 +123,17 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
if (query.getAggregation() == Aggregation.NONE) { if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityId, query); return findAllAsyncWithLimit(entityId, query);
} else { } else {
long stepTs = query.getStartTs();
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
while (stepTs < query.getEndTs()) { long endPeriod = query.getEndTs();
long startTs = stepTs; long startPeriod = query.getStartTs();
long endTs = stepTs + query.getInterval(); long step = query.getInterval();
while (startPeriod <= endPeriod) {
long startTs = startPeriod;
long endTs = Math.min(startPeriod + step, endPeriod + 1);
long ts = startTs + (endTs - startTs) / 2; long ts = startTs + (endTs - startTs) / 2;
futures.add(findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation())); ListenableFuture<Optional<TsKvEntry>> aggregateTsKvEntry = findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation());
stepTs = endTs; futures.add(aggregateTsKvEntry);
startPeriod = endTs;
} }
return getTskvEntriesFuture(Futures.allAsList(futures)); return getTskvEntriesFuture(Futures.allAsList(futures));
} }
@ -148,7 +152,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
return Futures.immediateFuture(DaoUtil.convertDataList(tsKvEntities)); return Futures.immediateFuture(DaoUtil.convertDataList(tsKvEntities));
} }
private ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) { ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) {
List<CompletableFuture<TsKvEntity>> entitiesFutures = new ArrayList<>(); List<CompletableFuture<TsKvEntity>> entitiesFutures = new ArrayList<>();
switchAggregation(entityId, key, startTs, endTs, aggregation, entitiesFutures); switchAggregation(entityId, key, startTs, endTs, aggregation, entitiesFutures);
return Futures.transform(setFutures(entitiesFutures), entity -> { return Futures.transform(setFutures(entitiesFutures), entity -> {

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java

@ -91,7 +91,7 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries
.stream() .stream()
.map(query -> findAllAsync(tenantId, entityId, query)) .map(query -> findAllAsync(tenantId, entityId, query))
.collect(Collectors.toList()); .collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<List<List<TsKvEntry>>, List<TsKvEntry>>() { return Futures.transform(Futures.allAsList(futures), new Function<>() {
@Nullable @Nullable
@Override @Override
public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) { public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) {

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java

@ -26,4 +26,4 @@ import java.util.List;
public interface AggregationTimeseriesDao { public interface AggregationTimeseriesDao {
ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query); ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query);
} }

3
dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java

@ -39,9 +39,7 @@ import java.util.stream.Collectors;
public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeningExecutorService { public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeningExecutorService {
private final ConcurrentMap<String, Integer> tsKvDictionaryMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, Integer> tsKvDictionaryMap = new ConcurrentHashMap<>();
protected static final ReentrantLock tsCreationLock = new ReentrantLock(); protected static final ReentrantLock tsCreationLock = new ReentrantLock();
@Autowired @Autowired
protected TsKvDictionaryRepository dictionaryRepository; protected TsKvDictionaryRepository dictionaryRepository;
@ -96,4 +94,5 @@ public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeni
} }
}, service); }, service);
} }
} }

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

@ -141,7 +141,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Override @Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) { public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList()); List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<List<List<TsKvEntry>>, List<TsKvEntry>>() { return Futures.transform(Futures.allAsList(futures), new Function<>() {
@Nullable @Nullable
@Override @Override
public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) { public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) {
@ -270,18 +270,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
if (query.getAggregation() == Aggregation.NONE) { if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(tenantId, entityId, query); return findAllAsyncWithLimit(tenantId, entityId, query);
} else { } else {
long startPeriod = query.getStartTs();
long endPeriod = query.getEndTs();
long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS); long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS);
long stepTs = query.getStartTs();
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
while (stepTs < query.getEndTs()) { while (startPeriod <= endPeriod) {
long startTs = stepTs; long startTs = startPeriod;
long endTs = stepTs + step; long endTs = Math.min(startPeriod + step, endPeriod + 1);
ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation(), query.getOrder()); long ts = endTs - startTs;
ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder());
futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
stepTs = endTs; startPeriod = endTs;
} }
ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures); ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures);
return Futures.transform(future, new Function<List<Optional<TsKvEntry>>, List<TsKvEntry>>() { return Futures.transform(future, new Function<>() {
@Nullable @Nullable
@Override @Override
public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> input) { public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> input) {

134
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -184,6 +184,140 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(2)); Assert.assertEquals(toTsEntry(TS - 1, stringKvEntry), entries.get(2));
} }
@Test
public void testFindByQuery_whenPeriodEqualsOneMilisecondPeriod() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L);
saveEntries(deviceId, TS);
saveEntries(deviceId, TS + 1L);
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS, 1, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS, new LongDataEntry(LONG_KEY, 1L)), entries.get(0));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS, new LongDataEntry(LONG_KEY, 1L)), entries.get(0));
}
@Test
public void testFindByQuery_whenPeriodEqualsInterval() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L);
for (long i = TS; i <= TS + 100L; i += 10L) {
saveEntries(deviceId, i);
}
saveEntries(deviceId, TS + 100L + 1L);
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 100, 101, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 11L)), entries.get(0));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 11L)), entries.get(0));
}
@Test
public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L);
for (long i = TS; i <= TS + 100000L; i += 10000L) {
saveEntries(deviceId, i);
}
saveEntries(deviceId, TS + 100000L + 1L);
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 99999, 50000, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
}
@Test
public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
saveEntries(deviceId, TS - 1L);
for (long i = TS; i <= TS + 80000L; i += 10000L) {
saveEntries(deviceId, i);
}
saveEntries(deviceId, TS + 80000L + 1L);
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 80000, 50000, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 4L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 4L)), entries.get(1));
}
@Test
public void testFindByQuery_whenPeriodHaveTwoIntervalWithEqualsLength_whereNotAllEntriesInRange() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000) {
saveEntries(deviceId, i);
}
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 99999, 50000, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
}
@Test
public void testFindByQuery_whenPeriodHaveTwoInterval_whereSecondShorterThanFirst_andNotAllEntriesInRange() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased());
for (long i = TS - 1L; i <= TS + 100000L + 1L; i += 10000L) {
saveEntries(deviceId, i);
}
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 80000, 50000, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 3L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 3L)), entries.get(1));
}
@Test @Test
public void testFindByQueryDescOrder() throws Exception { public void testFindByQueryDescOrder() throws Exception {
DeviceId deviceId = new DeviceId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased());

156
dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java

@ -0,0 +1,156 @@
/**
* Copyright © 2016-2022 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.sqlts;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.willCallRealMethod;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.thingsboard.server.common.data.id.TenantId.SYS_TENANT_ID;
import static org.thingsboard.server.common.data.kv.Aggregation.COUNT;
public class AbstractChunkedAggregationTimeseriesDaoTest {
final int LIMIT = 1;
final String TEMP = "temp";
final String DESC = "DESC";
AbstractChunkedAggregationTimeseriesDao tsDao;
@Before
public void setUp() throws Exception {
tsDao = spy(AbstractChunkedAggregationTimeseriesDao.class);
ListenableFuture<Optional<TsKvEntry>> optionalListenableFuture = Futures.immediateFuture(Optional.of(mock(TsKvEntry.class)));
willReturn(optionalListenableFuture).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any());
willReturn(Futures.immediateFuture(mock(TsKvEntry.class))).given(tsDao).getTskvEntriesFuture(any());
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenLastIntervalShorterThanOthersAndEqualsEndTs() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2000, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 2001, 1001, LIMIT, COUNT, DESC);
ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 2001, 3001, 2501, LIMIT, COUNT, DESC);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 2001, getTsForReadTsKvQuery(1, 2001), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 2001, 3000 + 1, getTsForReadTsKvQuery(2001, 3001), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriod() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 3000, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3001, 1501, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
assertThat(tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query)).isNotNull();
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000 + 1, getTsForReadTsKvQuery(1, 3001), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriodMinusOne() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2999, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3000, 1500, LIMIT, COUNT, DESC);
ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 3000, 3001, 3000, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 3000, 3001, getTsForReadTsKvQuery(3000, 3001), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriodPlusOne() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 3001, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3001, 1501, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3001, getTsForReadTsKvQuery(1, 3001), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsOneMillisecondAndStartTsIsZero() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 0, 0, 1, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 0, 1, 0, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 0, 1, getTsForReadTsKvQuery(0, 1), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsOneMillisecondAndStartTsIsOne() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 1, 1, LIMIT, COUNT, DESC);
ReadTsKvQuery subQuery = new BaseReadTsKvQuery(TEMP, 1, 2, 1, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuery.getKey(), 1, 2, getTsForReadTsKvQuery(1, 2), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsOneMillisecondAndStartTsIsIntegerMax() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, Integer.MAX_VALUE, Integer.MAX_VALUE, 1, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, Integer.MAX_VALUE, Integer.MAX_VALUE + 1L, Integer.MAX_VALUE, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), Integer.MAX_VALUE, 1L + Integer.MAX_VALUE, getTsForReadTsKvQuery(Integer.MAX_VALUE, 1L + Integer.MAX_VALUE), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsBigNumber() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, Integer.MAX_VALUE, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3001, 1501, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3001, getTsForReadTsKvQuery(1, 3001), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenCountIntervalEqualsPeriodSize() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 3, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1000)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
for (long i = 1; i <= 3000; i += 3) {
ReadTsKvQuery querySub = new BaseReadTsKvQuery(TEMP, i, i + 3, i + (i + 3 - i) / 2, LIMIT, COUNT, DESC);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, querySub.getKey(), i, i + 3, getTsForReadTsKvQuery(i, i + 3), COUNT);
}
}
long getTsForReadTsKvQuery(long startTs, long endTs) {
return startTs + (endTs - startTs) / 2L;
}
}

3
docker/.env

@ -1,5 +1,8 @@
TB_QUEUE_TYPE=kafka TB_QUEUE_TYPE=kafka
# redis or redis-cluster
CACHE=redis
DOCKER_REPO=thingsboard DOCKER_REPO=thingsboard
JS_EXECUTOR_DOCKER_NAME=tb-js-executor JS_EXECUTOR_DOCKER_NAME=tb-js-executor

7
docker/README.md

@ -17,6 +17,13 @@ In order to set database type change the value of `DATABASE` variable in `.env`
**NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.hybrid.yml` for details). **NOTE**: According to the database type corresponding docker service will be deployed (see `docker-compose.postgres.yml`, `docker-compose.hybrid.yml` for details).
In order to set cache type change the value of `CACHE` variable in `.env` file to one of the following:
- `redis` - use Redis standalone cache (1 node - 1 master);
- `redis-cluster` - use Redis cluster cache (6 nodes - 3 masters, 3 slaves);
**NOTE**: According to the cache type corresponding docker service will be deployed (see `docker-compose.redis.yml`, `docker-compose.redis-cluster.yml` for details).
Execute the following command to create log folders for the services and chown of these folders to the docker container users. Execute the following command to create log folders for the services and chown of these folders to the docker container users.
To be able to change user, **chown** command is used, which requires sudo permissions (script will request password for a sudo access): To be able to change user, **chown** command is used, which requires sudo permissions (script will request password for a sudo access):

5
docker/cache-redis-cluster.env

@ -0,0 +1,5 @@
CACHE_TYPE=redis
REDIS_CONNECTION_TYPE=cluster
REDIS_NODES=redis-node-0:6379,redis-node-1:6379,redis-node-2:6379,redis-node-3:6379,redis-node-4:6379,redis-node-5:6379
REDIS_USE_DEFAULT_POOL_CONFIG=false
REDIS_PASSWORD=thingsboard

2
docker/cache-redis.env

@ -0,0 +1,2 @@
CACHE_TYPE=redis
REDIS_HOST=redis

36
docker/compose-utils.sh

@ -73,19 +73,51 @@ function additionalComposeMonitoringArgs() {
fi fi
} }
function additionalComposeCacheArgs() {
source .env
CACHE_COMPOSE_ARGS=""
CACHE="${CACHE:-redis}"
case $CACHE in
redis)
CACHE_COMPOSE_ARGS="-f docker-compose.redis.yml"
;;
redis-cluster)
CACHE_COMPOSE_ARGS="-f docker-compose.redis-cluster.yml"
;;
*)
echo "Unknown CACHE value specified: '${CACHE}'. Should be either redis or redis-cluster." >&2
exit 1
esac
echo $CACHE_COMPOSE_ARGS
}
function additionalStartupServices() { function additionalStartupServices() {
source .env source .env
ADDITIONAL_STARTUP_SERVICES="" ADDITIONAL_STARTUP_SERVICES=""
case $DATABASE in case $DATABASE in
postgres) postgres)
ADDITIONAL_STARTUP_SERVICES=postgres ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES postgres"
;; ;;
hybrid) hybrid)
ADDITIONAL_STARTUP_SERVICES="postgres cassandra" ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES postgres cassandra"
;; ;;
*) *)
echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or hybrid." >&2 echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or hybrid." >&2
exit 1 exit 1
esac esac
CACHE="${CACHE:-redis}"
case $CACHE in
redis)
ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES redis"
;;
redis-cluster)
ADDITIONAL_STARTUP_SERVICES="$ADDITIONAL_STARTUP_SERVICES redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5"
;;
*)
echo "Unknown CACHE value specified: '${CACHE}'. Should be either redis or redis-cluster." >&2
exit 1
esac
echo $ADDITIONAL_STARTUP_SERVICES echo $ADDITIONAL_STARTUP_SERVICES
} }

32
docker/docker-compose.aws-sqs.yml

@ -23,59 +23,39 @@ services:
tb-core1: tb-core1:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
- redis
tb-core2: tb-core2:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
- redis
tb-rule-engine1: tb-rule-engine1:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
- redis
tb-rule-engine2: tb-rule-engine2:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
- redis
tb-mqtt-transport1: tb-mqtt-transport1:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
tb-mqtt-transport2: tb-mqtt-transport2:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
tb-http-transport1: tb-http-transport1:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
tb-http-transport2: tb-http-transport2:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
tb-coap-transport: tb-coap-transport:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
tb-lwm2m-transport: tb-lwm2m-transport:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on:
- zookeeper
tb-snmp-transport: tb-snmp-transport:
env_file: env_file:
- queue-aws-sqs.env - queue-aws-sqs.env
depends_on: tb-vc-executor1:
- zookeeper env_file:
- queue-aws-sqs.env
tb-vc-executor2:
env_file:
- queue-aws-sqs.env

8
docker/docker-compose.confluent.yml

@ -23,23 +23,15 @@ services:
tb-core1: tb-core1:
env_file: env_file:
- queue-confluent.env - queue-confluent.env
depends_on:
- redis
tb-core2: tb-core2:
env_file: env_file:
- queue-confluent.env - queue-confluent.env
depends_on:
- redis
tb-rule-engine1: tb-rule-engine1:
env_file: env_file:
- queue-confluent.env - queue-confluent.env
depends_on:
- redis
tb-rule-engine2: tb-rule-engine2:
env_file: env_file:
- queue-confluent.env - queue-confluent.env
depends_on:
- redis
tb-mqtt-transport1: tb-mqtt-transport1:
env_file: env_file:
- queue-confluent.env - queue-confluent.env

4
docker/docker-compose.hybrid.yml

@ -39,7 +39,6 @@ services:
- tb-node.hybrid.env - tb-node.hybrid.env
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- postgres - postgres
- cassandra - cassandra
tb-core2: tb-core2:
@ -47,7 +46,6 @@ services:
- tb-node.hybrid.env - tb-node.hybrid.env
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- postgres - postgres
- cassandra - cassandra
tb-rule-engine1: tb-rule-engine1:
@ -55,7 +53,6 @@ services:
- tb-node.hybrid.env - tb-node.hybrid.env
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- postgres - postgres
- cassandra - cassandra
tb-rule-engine2: tb-rule-engine2:
@ -63,6 +60,5 @@ services:
- tb-node.hybrid.env - tb-node.hybrid.env
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- postgres - postgres
- cassandra - cassandra

4
docker/docker-compose.kafka.yml

@ -36,25 +36,21 @@ services:
- queue-kafka.env - queue-kafka.env
depends_on: depends_on:
- kafka - kafka
- redis
tb-core2: tb-core2:
env_file: env_file:
- queue-kafka.env - queue-kafka.env
depends_on: depends_on:
- kafka - kafka
- redis
tb-rule-engine1: tb-rule-engine1:
env_file: env_file:
- queue-kafka.env - queue-kafka.env
depends_on: depends_on:
- kafka - kafka
- redis
tb-rule-engine2: tb-rule-engine2:
env_file: env_file:
- queue-kafka.env - queue-kafka.env
depends_on: depends_on:
- kafka - kafka
- redis
tb-mqtt-transport1: tb-mqtt-transport1:
env_file: env_file:
- queue-kafka.env - queue-kafka.env

61
docker/docker-compose.postgres.volumes.yml

@ -20,69 +20,8 @@ services:
postgres: postgres:
volumes: volumes:
- postgres-db-volume:/var/lib/postgresql/data - postgres-db-volume:/var/lib/postgresql/data
tb-core1:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-core2:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-rule-engine1:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-rule-engine2:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-coap-transport:
volumes:
- tb-coap-transport-log-volume:/var/log/tb-coap-transport
tb-lwm2m-transport:
volumes:
- tb-lwm2m-transport-log-volume:/var/log/tb-lwm2m-transport
tb-http-transport1:
volumes:
- tb-http-transport-log-volume:/var/log/tb-http-transport
tb-http-transport2:
volumes:
- tb-http-transport-log-volume:/var/log/tb-http-transport
tb-mqtt-transport1:
volumes:
- tb-mqtt-transport-log-volume:/var/log/tb-mqtt-transport
tb-mqtt-transport2:
volumes:
- tb-mqtt-transport-log-volume:/var/log/tb-mqtt-transport
tb-snmp-transport:
volumes:
- tb-snmp-transport-log-volume:/var/log/tb-snmp-transport
tb-vc-executor1:
volumes:
- tb-vc-executor-log-volume:/var/log/tb-vc-executor
tb-vc-executor2:
volumes:
- tb-vc-executor-log-volume:/var/log/tb-vc-executor
volumes: volumes:
postgres-db-volume: postgres-db-volume:
external: true external: true
name: ${POSTGRES_DATA_VOLUME} name: ${POSTGRES_DATA_VOLUME}
tb-log-volume:
external: true
name: ${TB_LOG_VOLUME}
tb-coap-transport-log-volume:
external: true
name: ${TB_COAP_TRANSPORT_LOG_VOLUME}
tb-lwm2m-transport-log-volume:
external: true
name: ${TB_LWM2M_TRANSPORT_LOG_VOLUME}
tb-http-transport-log-volume:
external: true
name: ${TB_HTTP_TRANSPORT_LOG_VOLUME}
tb-mqtt-transport-log-volume:
external: true
name: ${TB_MQTT_TRANSPORT_LOG_VOLUME}
tb-snmp-transport-log-volume:
external: true
name: ${TB_SNMP_TRANSPORT_LOG_VOLUME}
tb-vc-executor-log-volume:
external: true
name: ${TB_VC_EXECUTOR_LOG_VOLUME}

8
docker/docker-compose.postgres.yml

@ -31,27 +31,19 @@ services:
env_file: env_file:
- tb-node.postgres.env - tb-node.postgres.env
depends_on: depends_on:
- zookeeper
- redis
- postgres - postgres
tb-core2: tb-core2:
env_file: env_file:
- tb-node.postgres.env - tb-node.postgres.env
depends_on: depends_on:
- zookeeper
- redis
- postgres - postgres
tb-rule-engine1: tb-rule-engine1:
env_file: env_file:
- tb-node.postgres.env - tb-node.postgres.env
depends_on: depends_on:
- zookeeper
- redis
- postgres - postgres
tb-rule-engine2: tb-rule-engine2:
env_file: env_file:
- tb-node.postgres.env - tb-node.postgres.env
depends_on: depends_on:
- zookeeper
- redis
- postgres - postgres

58
docker/docker-compose.redis-cluster.volumes.yml

@ -0,0 +1,58 @@
#
# Copyright © 2016-2022 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.
#
version: '2.2'
services:
# Redis cluster
redis-node-0:
volumes:
- redis-cluster-data-0:/bitnami/redis/data
redis-node-1:
volumes:
- redis-cluster-data-1:/bitnami/redis/data
redis-node-2:
volumes:
- redis-cluster-data-2:/bitnami/redis/data
redis-node-3:
volumes:
- redis-cluster-data-3:/bitnami/redis/data
redis-node-4:
volumes:
- redis-cluster-data-4:/bitnami/redis/data
redis-node-5:
volumes:
- redis-cluster-data-5:/bitnami/redis/data
volumes:
redis-cluster-data-0:
external: true
name: ${REDIS_CLUSTER_DATA_VOLUME_0}
redis-cluster-data-1:
external: true
name: ${REDIS_CLUSTER_DATA_VOLUME_1}
redis-cluster-data-2:
external: true
name: ${REDIS_CLUSTER_DATA_VOLUME_2}
redis-cluster-data-3:
external: true
name: ${REDIS_CLUSTER_DATA_VOLUME_3}
redis-cluster-data-4:
external: true
name: ${REDIS_CLUSTER_DATA_VOLUME_4}
redis-cluster-data-5:
external: true
name: ${REDIS_CLUSTER_DATA_VOLUME_5}

148
docker/docker-compose.redis-cluster.yml

@ -0,0 +1,148 @@
#
# Copyright © 2016-2022 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.
#
version: '2.2'
services:
# Redis cluster
redis-node-0:
image: bitnami/redis-cluster:7.0
volumes:
- ./tb-node/redis-cluster-data-0:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=thingsboard'
- 'REDISCLI_AUTH=thingsboard'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
redis-node-1:
image: bitnami/redis-cluster:7.0
volumes:
- ./tb-node/redis-cluster-data-1:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=thingsboard'
- 'REDISCLI_AUTH=thingsboard'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
redis-node-2:
image: bitnami/redis-cluster:7.0
volumes:
- ./tb-node/redis-cluster-data-2:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=thingsboard'
- 'REDISCLI_AUTH=thingsboard'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
redis-node-3:
image: bitnami/redis-cluster:7.0
volumes:
- ./tb-node/redis-cluster-data-3:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=thingsboard'
- 'REDISCLI_AUTH=thingsboard'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
redis-node-4:
image: bitnami/redis-cluster:7.0
volumes:
- ./tb-node/redis-cluster-data-4:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=thingsboard'
- 'REDISCLI_AUTH=thingsboard'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
redis-node-5:
image: bitnami/redis-cluster:7.0
volumes:
- ./tb-node/redis-cluster-data-5:/bitnami/redis/data
depends_on:
- redis-node-0
- redis-node-1
- redis-node-2
- redis-node-3
- redis-node-4
environment:
- 'REDIS_PASSWORD=thingsboard'
- 'REDISCLI_AUTH=thingsboard'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
- 'REDIS_CLUSTER_REPLICAS=1'
- 'REDIS_CLUSTER_CREATOR=yes'
# ThingsBoard setup to use redis-cluster
tb-core1:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-core2:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-rule-engine1:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-rule-engine2:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-mqtt-transport1:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-mqtt-transport2:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-http-transport1:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-http-transport2:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-coap-transport:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-lwm2m-transport:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-snmp-transport:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-vc-executor1:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5
tb-vc-executor2:
env_file:
- cache-redis-cluster.env
depends_on:
- redis-node-5

27
docker/docker-compose.redis.volumes.yml

@ -0,0 +1,27 @@
#
# Copyright © 2016-2022 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.
#
version: '2.2'
services:
redis:
volumes:
- redis-data:/bitnami/redis/data
volumes:
redis-data:
external: true
name: ${REDIS_DATA_VOLUME}

97
docker/docker-compose.redis.yml

@ -0,0 +1,97 @@
#
# Copyright © 2016-2022 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.
#
version: '2.2'
services:
# Redis standalone
redis:
restart: always
image: bitnami/redis:7.0
environment:
# ALLOW_EMPTY_PASSWORD is recommended only for development.
ALLOW_EMPTY_PASSWORD: "yes"
ports:
- '6379:6379'
volumes:
- ./tb-node/redis-data:/bitnami/redis/data
# ThingsBoard setup to use redis-standalone
tb-core1:
env_file:
- cache-redis.env
depends_on:
- redis
tb-core2:
env_file:
- cache-redis.env
depends_on:
- redis
tb-rule-engine1:
env_file:
- cache-redis.env
depends_on:
- redis
tb-rule-engine2:
env_file:
- cache-redis.env
depends_on:
- redis
tb-mqtt-transport1:
env_file:
- cache-redis.env
depends_on:
- redis
tb-mqtt-transport2:
env_file:
- cache-redis.env
depends_on:
- redis
tb-http-transport1:
env_file:
- cache-redis.env
depends_on:
- redis
tb-http-transport2:
env_file:
- cache-redis.env
depends_on:
- redis
tb-coap-transport:
env_file:
- cache-redis.env
depends_on:
- redis
tb-lwm2m-transport:
env_file:
- cache-redis.env
depends_on:
- redis
tb-snmp-transport:
env_file:
- cache-redis.env
depends_on:
- redis
tb-vc-executor1:
env_file:
- cache-redis.env
depends_on:
- redis
tb-vc-executor2:
env_file:
- cache-redis.env
depends_on:
- redis

81
docker/docker-compose.volumes.yml

@ -0,0 +1,81 @@
#
# Copyright © 2016-2022 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.
#
version: '2.2'
services:
tb-core1:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-core2:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-rule-engine1:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-rule-engine2:
volumes:
- tb-log-volume:/var/log/thingsboard
tb-coap-transport:
volumes:
- tb-coap-transport-log-volume:/var/log/tb-coap-transport
tb-lwm2m-transport:
volumes:
- tb-lwm2m-transport-log-volume:/var/log/tb-lwm2m-transport
tb-http-transport1:
volumes:
- tb-http-transport-log-volume:/var/log/tb-http-transport
tb-http-transport2:
volumes:
- tb-http-transport-log-volume:/var/log/tb-http-transport
tb-mqtt-transport1:
volumes:
- tb-mqtt-transport-log-volume:/var/log/tb-mqtt-transport
tb-mqtt-transport2:
volumes:
- tb-mqtt-transport-log-volume:/var/log/tb-mqtt-transport
tb-snmp-transport:
volumes:
- tb-snmp-transport-log-volume:/var/log/tb-snmp-transport
tb-vc-executor1:
volumes:
- tb-vc-executor-log-volume:/var/log/tb-vc-executor
tb-vc-executor2:
volumes:
- tb-vc-executor-log-volume:/var/log/tb-vc-executor
volumes:
tb-log-volume:
external: true
name: ${TB_LOG_VOLUME}
tb-coap-transport-log-volume:
external: true
name: ${TB_COAP_TRANSPORT_LOG_VOLUME}
tb-lwm2m-transport-log-volume:
external: true
name: ${TB_LWM2M_TRANSPORT_LOG_VOLUME}
tb-http-transport-log-volume:
external: true
name: ${TB_HTTP_TRANSPORT_LOG_VOLUME}
tb-mqtt-transport-log-volume:
external: true
name: ${TB_MQTT_TRANSPORT_LOG_VOLUME}
tb-snmp-transport-log-volume:
external: true
name: ${TB_SNMP_TRANSPORT_LOG_VOLUME}
tb-vc-executor-log-volume:
external: true
name: ${TB_VC_EXECUTOR_LOG_VOLUME}

14
docker/docker-compose.yml

@ -20,21 +20,17 @@ version: '2.2'
services: services:
zookeeper: zookeeper:
restart: always restart: always
image: "zookeeper:3.5" image: "zookeeper:3.8.0"
ports: ports:
- "2181" - "2181"
environment: environment:
ZOO_MY_ID: 1 ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zookeeper:2888:3888;zookeeper:2181 ZOO_SERVERS: server.1=zookeeper:2888:3888;zookeeper:2181
redis: ZOO_ADMINSERVER_ENABLED: "false"
restart: always
image: redis:4.0
ports:
- "6379"
tb-js-executor: tb-js-executor:
restart: always restart: always
image: "${DOCKER_REPO}/${JS_EXECUTOR_DOCKER_NAME}:${TB_VERSION}" image: "${DOCKER_REPO}/${JS_EXECUTOR_DOCKER_NAME}:${TB_VERSION}"
scale: 20 scale: 10
env_file: env_file:
- tb-js-executor.env - tb-js-executor.env
tb-core1: tb-core1:
@ -59,7 +55,6 @@ services:
- ./tb-node/log:/var/log/thingsboard - ./tb-node/log:/var/log/thingsboard
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- tb-js-executor - tb-js-executor
- tb-rule-engine1 - tb-rule-engine1
- tb-rule-engine2 - tb-rule-engine2
@ -85,7 +80,6 @@ services:
- ./tb-node/log:/var/log/thingsboard - ./tb-node/log:/var/log/thingsboard
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- tb-js-executor - tb-js-executor
- tb-rule-engine1 - tb-rule-engine1
- tb-rule-engine2 - tb-rule-engine2
@ -109,7 +103,6 @@ services:
- ./tb-node/log:/var/log/thingsboard - ./tb-node/log:/var/log/thingsboard
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- tb-js-executor - tb-js-executor
tb-rule-engine2: tb-rule-engine2:
restart: always restart: always
@ -131,7 +124,6 @@ services:
- ./tb-node/log:/var/log/thingsboard - ./tb-node/log:/var/log/thingsboard
depends_on: depends_on:
- zookeeper - zookeeper
- redis
- tb-js-executor - tb-js-executor
tb-mqtt-transport1: tb-mqtt-transport1:
restart: always restart: always

21
docker/docker-create-log-folders.sh

@ -28,3 +28,24 @@ mkdir -p tb-transports/mqtt/log && sudo chown -R 799:799 tb-transports/mqtt/log
mkdir -p tb-transports/snmp/log && sudo chown -R 799:799 tb-transports/snmp/log mkdir -p tb-transports/snmp/log && sudo chown -R 799:799 tb-transports/snmp/log
mkdir -p tb-vc-executor/log && sudo chown -R 799:799 tb-vc-executor/log mkdir -p tb-vc-executor/log && sudo chown -R 799:799 tb-vc-executor/log
mkdir -p tb-node/postgres/ && sudo chown -R 999:999 tb-node/postgres/
source .env
CACHE="${CACHE:-redis}"
case $CACHE in
redis)
mkdir -p tb-node/redis-data/ && sudo chown -R 1001:0 tb-node/redis-data/
;;
redis-cluster)
mkdir -p tb-node/redis-cluster-data-0/ && sudo chown -R 1001:0 tb-node/redis-cluster-data-0/
mkdir -p tb-node/redis-cluster-data-1/ && sudo chown -R 1001:0 tb-node/redis-cluster-data-1/
mkdir -p tb-node/redis-cluster-data-2/ && sudo chown -R 1001:0 tb-node/redis-cluster-data-2/
mkdir -p tb-node/redis-cluster-data-3/ && sudo chown -R 1001:0 tb-node/redis-cluster-data-3/
mkdir -p tb-node/redis-cluster-data-4/ && sudo chown -R 1001:0 tb-node/redis-cluster-data-4/
mkdir -p tb-node/redis-cluster-data-5/ && sudo chown -R 1001:0 tb-node/redis-cluster-data-5/
;;
*)
echo "Unknown CACHE value specified: '${CACHE}'. Should be either redis or redis-cluster." >&2
exit 1
esac

6
docker/docker-install-tb.sh

@ -45,12 +45,14 @@ ADDITIONAL_COMPOSE_QUEUE_ARGS=$(additionalComposeQueueArgs) || exit $?
ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $?
ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $?
if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then if [ ! -z "${ADDITIONAL_STARTUP_SERVICES// }" ]; then
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS up -d redis $ADDITIONAL_STARTUP_SERVICES docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS up -d $ADDITIONAL_STARTUP_SERVICES
fi fi
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=${loadDemo} tb-core1 docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=${loadDemo} tb-core1

4
docker/docker-remove-services.sh

@ -23,6 +23,8 @@ ADDITIONAL_COMPOSE_QUEUE_ARGS=$(additionalComposeQueueArgs) || exit $?
ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $?
ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $?
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS $ADDITIONAL_COMPOSE_MONITORING_ARGS down -v docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS $ADDITIONAL_COMPOSE_MONITORING_ARGS down -v

4
docker/docker-start-services.sh

@ -23,6 +23,8 @@ ADDITIONAL_COMPOSE_QUEUE_ARGS=$(additionalComposeQueueArgs) || exit $?
ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $?
ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $?
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS $ADDITIONAL_COMPOSE_MONITORING_ARGS up -d docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS $ADDITIONAL_COMPOSE_MONITORING_ARGS up -d

4
docker/docker-stop-services.sh

@ -23,6 +23,8 @@ ADDITIONAL_COMPOSE_QUEUE_ARGS=$(additionalComposeQueueArgs) || exit $?
ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $?
ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $? ADDITIONAL_COMPOSE_MONITORING_ARGS=$(additionalComposeMonitoringArgs) || exit $?
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS $ADDITIONAL_COMPOSE_MONITORING_ARGS stop docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS $ADDITIONAL_COMPOSE_MONITORING_ARGS stop

6
docker/docker-update-service.sh

@ -23,5 +23,7 @@ ADDITIONAL_COMPOSE_QUEUE_ARGS=$(additionalComposeQueueArgs) || exit $?
ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $?
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS pull $@ ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS up -d --no-deps --build $@
docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS pull $@
docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS up -d --no-deps --build $@

8
docker/docker-upgrade-tb.sh

@ -44,10 +44,12 @@ ADDITIONAL_COMPOSE_QUEUE_ARGS=$(additionalComposeQueueArgs) || exit $?
ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $? ADDITIONAL_COMPOSE_ARGS=$(additionalComposeArgs) || exit $?
ADDITIONAL_CACHE_ARGS=$(additionalComposeCacheArgs) || exit $?
ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $? ADDITIONAL_STARTUP_SERVICES=$(additionalStartupServices) || exit $?
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS pull tb-core1 docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS pull tb-core1
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS up -d redis $ADDITIONAL_STARTUP_SERVICES docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS up -d $ADDITIONAL_STARTUP_SERVICES
docker-compose -f docker-compose.yml $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS run --no-deps --rm -e UPGRADE_TB=true -e FROM_VERSION=${fromVersion} tb-core1 docker-compose -f docker-compose.yml $ADDITIONAL_CACHE_ARGS $ADDITIONAL_COMPOSE_ARGS $ADDITIONAL_COMPOSE_QUEUE_ARGS run --no-deps --rm -e UPGRADE_TB=true -e FROM_VERSION=${fromVersion} tb-core1

3
docker/tb-coap-transport.env

@ -10,6 +10,3 @@ METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081 HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

3
docker/tb-http-transport.env

@ -7,6 +7,3 @@ HTTP_REQUEST_TIMEOUT=60000
METRICS_ENABLED=true METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus METRICS_ENDPOINTS_EXPOSE=prometheus
CACHE_TYPE=redis
REDIS_HOST=redis

3
docker/tb-lwm2m-transport.env

@ -10,6 +10,3 @@ METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081 HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

3
docker/tb-mqtt-transport.env

@ -10,6 +10,3 @@ METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081 HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

2
docker/tb-node.env

@ -4,8 +4,6 @@ ZOOKEEPER_ENABLED=true
ZOOKEEPER_URL=zookeeper:2181 ZOOKEEPER_URL=zookeeper:2181
JS_EVALUATOR=remote JS_EVALUATOR=remote
TRANSPORT_TYPE=remote TRANSPORT_TYPE=remote
CACHE_TYPE=redis
REDIS_HOST=redis
HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false

3
docker/tb-snmp-transport.env

@ -6,6 +6,3 @@ METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081 HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

7
msa/black-box-tests/README.md

@ -18,8 +18,13 @@ As result, in REPOSITORY column, next images should be present:
thingsboard/tb-web-ui thingsboard/tb-web-ui
thingsboard/tb-js-executor thingsboard/tb-js-executor
- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory: - Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone:
mvn clean install -DblackBoxTests.skip=false mvn clean install -DblackBoxTests.skip=false
- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis cluster:
mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.redisCluster=true

13
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -39,7 +39,7 @@ import static org.junit.Assert.fail;
@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"}) @ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"})
@Slf4j @Slf4j
public class ContainerTestSuite { public class ContainerTestSuite {
final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster"));
private static final String SOURCE_DIR = "./../../docker/"; private static final String SOURCE_DIR = "./../../docker/";
private static final String TB_CORE_LOG_REGEXP = ".*Starting polling for events.*"; private static final String TB_CORE_LOG_REGEXP = ".*Starting polling for events.*";
private static final String TRANSPORTS_LOG_REGEXP = ".*Going to recalculate partitions.*"; private static final String TRANSPORTS_LOG_REGEXP = ".*Going to recalculate partitions.*";
@ -52,6 +52,7 @@ public class ContainerTestSuite {
@ClassRule @ClassRule
public static DockerComposeContainer getTestContainer() { public static DockerComposeContainer getTestContainer() {
if (testContainer == null) { if (testContainer == null) {
log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER);
boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers"));
try { try {
final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/"; final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/";
@ -73,9 +74,17 @@ public class ContainerTestSuite {
testContainer = new DockerComposeContainerImpl<>( testContainer = new DockerComposeContainerImpl<>(
new File(targetDir + "docker-compose.yml"), new File(targetDir + "docker-compose.yml"),
new File(targetDir + "docker-compose.volumes.yml"),
new File(targetDir + "docker-compose.postgres.yml"), new File(targetDir + "docker-compose.postgres.yml"),
new File(targetDir + "docker-compose.postgres.volumes.yml"), new File(targetDir + "docker-compose.postgres.volumes.yml"),
new File(targetDir + "docker-compose.kafka.yml")) new File(targetDir + "docker-compose.kafka.yml"),
IS_REDIS_CLUSTER
? new File("./../../docker/docker-compose.redis-cluster.yml")
: new File("./../../docker/docker-compose.redis.yml"),
IS_REDIS_CLUSTER
? new File("./../../docker/docker-compose.redis-cluster.volumes.yml")
: new File("./../../docker/docker-compose.redis.volumes.yml")
)
.withPull(false) .withPull(false)
.withLocalCompose(true) .withLocalCompose(true)
.withTailChildContainers(!skipTailChildContainers) .withTailChildContainers(!skipTailChildContainers)

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.msa; package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
import org.testcontainers.utility.Base58; import org.testcontainers.utility.Base58;
@ -24,10 +25,16 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Slf4j
public class ThingsBoardDbInstaller extends ExternalResource { public class ThingsBoardDbInstaller extends ExternalResource {
final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster"));
private final static String POSTGRES_DATA_VOLUME = "tb-postgres-test-data-volume"; private final static String POSTGRES_DATA_VOLUME = "tb-postgres-test-data-volume";
private final static String REDIS_DATA_VOLUME = "tb-redis-data-volume";
private final static String REDIS_CLUSTER_DATA_VOLUME = "tb-redis-cluster-data-volume";
private final static String TB_LOG_VOLUME = "tb-log-test-volume"; private final static String TB_LOG_VOLUME = "tb-log-test-volume";
private final static String TB_COAP_TRANSPORT_LOG_VOLUME = "tb-coap-transport-log-test-volume"; private final static String TB_COAP_TRANSPORT_LOG_VOLUME = "tb-coap-transport-log-test-volume";
private final static String TB_LWM2M_TRANSPORT_LOG_VOLUME = "tb-lwm2m-transport-log-test-volume"; private final static String TB_LWM2M_TRANSPORT_LOG_VOLUME = "tb-lwm2m-transport-log-test-volume";
@ -39,6 +46,9 @@ public class ThingsBoardDbInstaller extends ExternalResource {
private final DockerComposeExecutor dockerCompose; private final DockerComposeExecutor dockerCompose;
private final String postgresDataVolume; private final String postgresDataVolume;
private final String redisDataVolume;
private final String redisClusterDataVolume;
private final String tbLogVolume; private final String tbLogVolume;
private final String tbCoapTransportLogVolume; private final String tbCoapTransportLogVolume;
private final String tbLwm2mTransportLogVolume; private final String tbLwm2mTransportLogVolume;
@ -49,14 +59,26 @@ public class ThingsBoardDbInstaller extends ExternalResource {
private final Map<String, String> env; private final Map<String, String> env;
public ThingsBoardDbInstaller() { public ThingsBoardDbInstaller() {
List<File> composeFiles = Arrays.asList(new File("./../../docker/docker-compose.yml"), log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER);
List<File> composeFiles = Arrays.asList(
new File("./../../docker/docker-compose.yml"),
new File("./../../docker/docker-compose.volumes.yml"),
new File("./../../docker/docker-compose.postgres.yml"), new File("./../../docker/docker-compose.postgres.yml"),
new File("./../../docker/docker-compose.postgres.volumes.yml")); new File("./../../docker/docker-compose.postgres.volumes.yml"),
IS_REDIS_CLUSTER
? new File("./../../docker/docker-compose.redis-cluster.yml")
: new File("./../../docker/docker-compose.redis.yml"),
IS_REDIS_CLUSTER
? new File("./../../docker/docker-compose.redis-cluster.volumes.yml")
: new File("./../../docker/docker-compose.redis.volumes.yml")
);
String identifier = Base58.randomString(6).toLowerCase(); String identifier = Base58.randomString(6).toLowerCase();
String project = identifier + Base58.randomString(6).toLowerCase(); String project = identifier + Base58.randomString(6).toLowerCase();
postgresDataVolume = project + "_" + POSTGRES_DATA_VOLUME; postgresDataVolume = project + "_" + POSTGRES_DATA_VOLUME;
redisDataVolume = project + "_" + REDIS_DATA_VOLUME;
redisClusterDataVolume = project + "_" + REDIS_CLUSTER_DATA_VOLUME;
tbLogVolume = project + "_" + TB_LOG_VOLUME; tbLogVolume = project + "_" + TB_LOG_VOLUME;
tbCoapTransportLogVolume = project + "_" + TB_COAP_TRANSPORT_LOG_VOLUME; tbCoapTransportLogVolume = project + "_" + TB_COAP_TRANSPORT_LOG_VOLUME;
tbLwm2mTransportLogVolume = project + "_" + TB_LWM2M_TRANSPORT_LOG_VOLUME; tbLwm2mTransportLogVolume = project + "_" + TB_LWM2M_TRANSPORT_LOG_VOLUME;
@ -76,6 +98,13 @@ public class ThingsBoardDbInstaller extends ExternalResource {
env.put("TB_MQTT_TRANSPORT_LOG_VOLUME", tbMqttTransportLogVolume); env.put("TB_MQTT_TRANSPORT_LOG_VOLUME", tbMqttTransportLogVolume);
env.put("TB_SNMP_TRANSPORT_LOG_VOLUME", tbSnmpTransportLogVolume); env.put("TB_SNMP_TRANSPORT_LOG_VOLUME", tbSnmpTransportLogVolume);
env.put("TB_VC_EXECUTOR_LOG_VOLUME", tbVcExecutorLogVolume); env.put("TB_VC_EXECUTOR_LOG_VOLUME", tbVcExecutorLogVolume);
if (IS_REDIS_CLUSTER) {
for (int i = 0; i < 6; i++) {
env.put("REDIS_CLUSTER_DATA_VOLUME_" + i, redisClusterDataVolume + '-' + i);
}
} else {
env.put("REDIS_DATA_VOLUME", redisDataVolume);
}
dockerCompose.withEnv(env); dockerCompose.withEnv(env);
} }
@ -111,7 +140,20 @@ public class ThingsBoardDbInstaller extends ExternalResource {
dockerCompose.withCommand("volume create " + tbVcExecutorLogVolume); dockerCompose.withCommand("volume create " + tbVcExecutorLogVolume);
dockerCompose.invokeDocker(); dockerCompose.invokeDocker();
dockerCompose.withCommand("up -d redis postgres"); String redisService = "";
if (IS_REDIS_CLUSTER) {
for (int i = 0; i < 6; i++) {
redisService = redisService + " redis-node-" + i;
dockerCompose.withCommand("volume create " + redisClusterDataVolume + '-' + i);
dockerCompose.invokeDocker();
}
} else {
redisService = "redis";
dockerCompose.withCommand("volume create " + redisDataVolume);
dockerCompose.invokeDocker();
}
dockerCompose.withCommand("up -d postgres " + redisService);
dockerCompose.invokeCompose(); dockerCompose.invokeCompose();
dockerCompose.withCommand("run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=true tb-core1"); dockerCompose.withCommand("run --no-deps --rm -e INSTALL_TB=true -e LOAD_DEMO=true tb-core1");
@ -137,7 +179,10 @@ public class ThingsBoardDbInstaller extends ExternalResource {
dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume + dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume +
" " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume + " " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume +
" " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume); " " + tbMqttTransportLogVolume + " " + tbSnmpTransportLogVolume + " " + tbVcExecutorLogVolume +
(IS_REDIS_CLUSTER
? IntStream.range(0, 6).mapToObj(i -> " " + redisClusterDataVolume + '-' + i).collect(Collectors.joining())
: redisDataVolume));
dockerCompose.invokeDocker(); dockerCompose.invokeDocker();
} }

6
msa/js-executor/api/jsInvokeMessageProcessor.js

@ -144,7 +144,7 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function (requestId, r
}, },
(err) => { (err) => {
var errorCode; var errorCode;
if (err.message.includes('Script execution timed out')) { if (err && isString(err.message) && err.message.includes('Script execution timed out')) {
errorCode = TIMEOUT_ERROR; errorCode = TIMEOUT_ERROR;
} else { } else {
errorCode = RUNTIME_ERROR; errorCode = RUNTIME_ERROR;
@ -321,4 +321,8 @@ function deleteMinUsedScript() {
this.scriptMap.delete(prevScriptId); this.scriptMap.delete(prevScriptId);
} }
function isString(value) {
return typeof value === 'string';
}
module.exports = JsInvokeMessageProcessor; module.exports = JsInvokeMessageProcessor;

12
ui-ngx/src/app/core/http/entities-version-control.service.ts

@ -118,20 +118,23 @@ export class EntitiesVersionControlService {
public listEntityVersions(pageLink: PageLink, branch: string, public listEntityVersions(pageLink: PageLink, branch: string,
externalEntityId: EntityId, externalEntityId: EntityId,
config?: RequestConfig): Observable<PageData<EntityVersion>> { config?: RequestConfig): Observable<PageData<EntityVersion>> {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}/${externalEntityId.entityType}/${externalEntityId.id}${pageLink.toQuery()}`, const encodedBranch = encodeURIComponent(branch);
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${externalEntityId.entityType}/${externalEntityId.id}${pageLink.toQuery()}&branch=${encodedBranch}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public listEntityTypeVersions(pageLink: PageLink, branch: string, public listEntityTypeVersions(pageLink: PageLink, branch: string,
entityType: EntityType, entityType: EntityType,
config?: RequestConfig): Observable<PageData<EntityVersion>> { config?: RequestConfig): Observable<PageData<EntityVersion>> {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}/${entityType}${pageLink.toQuery()}`, const encodedBranch = encodeURIComponent(branch);
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${entityType}${pageLink.toQuery()}&branch=${encodedBranch}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public listVersions(pageLink: PageLink, branch: string, public listVersions(pageLink: PageLink, branch: string,
config?: RequestConfig): Observable<PageData<EntityVersion>> { config?: RequestConfig): Observable<PageData<EntityVersion>> {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}${pageLink.toQuery()}`, const encodedBranch = encodeURIComponent(branch);
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version${pageLink.toQuery()}&branch=${encodedBranch}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
@ -160,7 +163,8 @@ export class EntitiesVersionControlService {
entityId: EntityId, entityId: EntityId,
versionId: string, versionId: string,
config?: RequestConfig): Observable<EntityDataDiff> { config?: RequestConfig): Observable<EntityDataDiff> {
return this.http.get<EntityDataDiff>(`/api/entities/vc/diff/${branch}/${entityId.entityType}/${entityId.id}?versionId=${versionId}`, const encodedBranch = encodeURIComponent(branch);
return this.http.get<EntityDataDiff>(`/api/entities/vc/diff/${entityId.entityType}/${entityId.id}?branch=${encodedBranch}&versionId=${versionId}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }

2
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<form (ngSubmit)="add()" style="min-width: 400px;" [ngStyle]="entitiesTableConfig.addDialogStyle"> <form (ngSubmit)="add()" style="min-width: 400px; width: 700px;" [ngStyle]="entitiesTableConfig.addDialogStyle">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>{{ translations.add }}</h2> <h2 translate>{{ translations.add }}</h2>
<span fxFlex></span> <span fxFlex></span>

4
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html

@ -48,7 +48,7 @@
<div class="mat-padding" fxLayout="column"> <div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm"> <form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> <fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.name</mat-label> <mat-label translate>tenant-profile.name</mat-label>
<input matInput formControlName="name" required/> <input matInput formControlName="name" required/>
<mat-error *ngIf="entityForm.get('name').hasError('required')"> <mat-error *ngIf="entityForm.get('name').hasError('required')">
@ -99,7 +99,7 @@
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
</div> </div>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.description</mat-label> <mat-label translate>tenant-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field> </mat-form-field>

4
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss

@ -14,6 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { :host ::ng-deep {
.mat-form-field-infix {
width: 100% !important;
}
.mat-checkbox{ .mat-checkbox{
&.hinted-checkbox { &.hinted-checkbox {
.mat-checkbox-inner-container { .mat-checkbox-inner-container {

888
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -16,387 +16,511 @@
--> -->
<section [formGroup]="defaultTenantProfileConfigurationFormGroup" fxLayout="column"> <section [formGroup]="defaultTenantProfileConfigurationFormGroup" fxLayout="column">
<mat-form-field class="mat-block"> <fieldset class="fields-group">
<mat-label translate>tenant-profile.maximum-devices</mat-label> <legend class="group-title">
<input matInput required min="0" step="1" {{ 'tenant-profile.entities' | translate }} <span translate>tenant-profile.unlimited</span>
formControlName="maxDevices" </legend>
type="number"> <div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDevices').hasError('required')"> <mat-form-field fxFlex class="mat-block" appearance="fill">
{{ 'tenant-profile.maximum-devices-required' | translate}} <mat-label translate>tenant-profile.maximum-devices</mat-label>
</mat-error> <input matInput required min="0" step="1"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDevices').hasError('min')"> formControlName="maxDevices"
{{ 'tenant-profile.maximum-devices-range' | translate}} type="number">
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDevices').hasError('required')">
</mat-form-field> {{ 'tenant-profile.maximum-devices-required' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.maximum-assets</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDevices').hasError('min')">
<input matInput required min="0" step="1" {{ 'tenant-profile.maximum-devices-range' | translate}}
formControlName="maxAssets" </mat-error>
type="number"> </mat-form-field>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxAssets').hasError('required')"> <mat-form-field fxFlex class="mat-block" appearance="fill">
{{ 'tenant-profile.maximum-assets-required' | translate}} <mat-label translate>tenant-profile.maximum-dashboards</mat-label>
</mat-error> <input matInput required min="0" step="1"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxAssets').hasError('min')"> formControlName="maxDashboards"
{{ 'tenant-profile.maximum-assets-range' | translate}} type="number">
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDashboards').hasError('required')">
</mat-form-field> {{ 'tenant-profile.maximum-dashboards-required' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.maximum-customers</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDashboards').hasError('min')">
<input matInput required min="0" step="1" {{ 'tenant-profile.maximum-dashboards-range' | translate}}
formControlName="maxCustomers" </mat-error>
type="number"> </mat-form-field>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCustomers').hasError('required')"> </div>
{{ 'tenant-profile.maximum-customers-required' | translate}} <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
</mat-error> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCustomers').hasError('min')"> <mat-label translate>tenant-profile.maximum-assets</mat-label>
{{ 'tenant-profile.maximum-customers-range' | translate}} <input matInput required min="0" step="1"
</mat-error> formControlName="maxAssets"
</mat-form-field> type="number">
<mat-form-field class="mat-block"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxAssets').hasError('required')">
<mat-label translate>tenant-profile.maximum-users</mat-label> {{ 'tenant-profile.maximum-assets-required' | translate}}
<input matInput required min="0" step="1" </mat-error>
formControlName="maxUsers" <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxAssets').hasError('min')">
type="number"> {{ 'tenant-profile.maximum-assets-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxUsers').hasError('required')"> </mat-error>
{{ 'tenant-profile.maximum-users-required' | translate}} </mat-form-field>
</mat-error> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxUsers').hasError('min')"> <mat-label translate>tenant-profile.maximum-users</mat-label>
{{ 'tenant-profile.maximum-users-range' | translate}} <input matInput required min="0" step="1"
</mat-error> formControlName="maxUsers"
</mat-form-field> type="number">
<mat-form-field class="mat-block"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxUsers').hasError('required')">
<mat-label translate>tenant-profile.maximum-dashboards</mat-label> {{ 'tenant-profile.maximum-users-required' | translate}}
<input matInput required min="0" step="1" </mat-error>
formControlName="maxDashboards" <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxUsers').hasError('min')">
type="number"> {{ 'tenant-profile.maximum-users-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDashboards').hasError('required')"> </mat-error>
{{ 'tenant-profile.maximum-dashboards-required' | translate}} </mat-form-field>
</mat-error> </div>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDashboards').hasError('min')"> <mat-expansion-panel class="configuration-panel">
{{ 'tenant-profile.maximum-dashboards-range' | translate}} <mat-expansion-panel-header>
</mat-error> <mat-panel-description fxLayoutAlign="end" translate>
</mat-form-field> tenant-profile.advanced-settings
<mat-form-field class="mat-block"> </mat-panel-description>
<mat-label translate>tenant-profile.maximum-rule-chains</mat-label> </mat-expansion-panel-header>
<input matInput required min="0" step="1" <ng-template matExpansionPanelContent>
formControlName="maxRuleChains" <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
type="number"> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleChains').hasError('required')"> <mat-label translate>tenant-profile.maximum-customers</mat-label>
{{ 'tenant-profile.maximum-rule-chains-required' | translate}} <input matInput required min="0" step="1"
</mat-error> formControlName="maxCustomers"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleChains').hasError('min')"> type="number">
{{ 'tenant-profile.maximum-rule-chains-range' | translate}} <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCustomers').hasError('required')">
</mat-error> {{ 'tenant-profile.maximum-customers-required' | translate}}
</mat-form-field> </mat-error>
<mat-form-field class="mat-block"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCustomers').hasError('min')">
<mat-label translate>tenant-profile.maximum-resources-sum-data-size</mat-label> {{ 'tenant-profile.maximum-customers-range' | translate}}
<input matInput required min="0" step="1" </mat-error>
formControlName="maxResourcesInBytes" </mat-form-field>
type="number"> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('required')"> <mat-label translate>tenant-profile.maximum-rule-chains</mat-label>
{{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} <input matInput required min="0" step="1"
</mat-error> formControlName="maxRuleChains"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('min')"> type="number">
{{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleChains').hasError('required')">
</mat-error> {{ 'tenant-profile.maximum-rule-chains-required' | translate}}
</mat-form-field> </mat-error>
<mat-form-field class="mat-block"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleChains').hasError('min')">
<mat-label translate>tenant-profile.maximum-ota-packages-sum-data-size</mat-label> {{ 'tenant-profile.maximum-rule-chains-range' | translate}}
<input matInput required min="0" step="1" </mat-error>
formControlName="maxOtaPackagesInBytes" </mat-form-field>
type="number"> </div>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('required')"> </ng-template>
{{ 'tenant-profile.maximum-ota-packages-sum-data-size-required' | translate}} </mat-expansion-panel>
</mat-error> </fieldset>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('min')">
{{ 'tenant-profile.maximum-ota-packages-sum-data-size-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-transport-messages</mat-label>
<input matInput required min="0" step="1"
formControlName="maxTransportMessages"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportMessages').hasError('required')">
{{ 'tenant-profile.max-transport-messages-range' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportMessages').hasError('min')">
{{ 'tenant-profile.max-transport-messages-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-transport-data-points</mat-label>
<input matInput required min="0" step="1"
formControlName="maxTransportDataPoints"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportDataPoints').hasError('required')">
{{ 'tenant-profile.max-transport-data-points-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportDataPoints').hasError('min')">
{{ 'tenant-profile.max-transport-data-points-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-r-e-executions</mat-label>
<input matInput required min="0" step="1"
formControlName="maxREExecutions"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxREExecutions').hasError('required')">
{{ 'tenant-profile.max-r-e-executions-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxREExecutions').hasError('min')">
{{ 'tenant-profile.max-r-e-executions-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-j-s-executions</mat-label>
<input matInput required min="0" step="1"
formControlName="maxJSExecutions"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxJSExecutions').hasError('required')">
{{ 'tenant-profile.max-j-s-executions-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxJSExecutions').hasError('min')">
{{ 'tenant-profile.max-j-s-executions-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-d-p-storage-days</mat-label>
<input matInput required min="0" step="1"
formControlName="maxDPStorageDays"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDPStorageDays').hasError('required')">
{{ 'tenant-profile.max-d-p-storage-days-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDPStorageDays').hasError('min')">
{{ 'tenant-profile.max-d-p-storage-days-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.default-storage-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="defaultStorageTtlDays"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('defaultStorageTtlDays').hasError('required')">
{{ 'tenant-profile.default-storage-ttl-days-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('defaultStorageTtlDays').hasError('min')">
{{ 'tenant-profile.default-storage-ttl-days-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.alarms-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="alarmsTtlDays"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('required')">
{{ 'tenant-profile.alarms-ttl-days-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('min')">
{{ 'tenant-profile.alarms-ttl-days-days-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.rpc-ttl-days</mat-label>
<input matInput required min="0" step="1"
formControlName="rpcTtlDays"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('rpcTtlDays').hasError('required')">
{{ 'tenant-profile.rpc-ttl-days-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('rpcTtlDays').hasError('min')">
{{ 'tenant-profile.rpc-ttl-days-days-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-rule-node-executions-per-message</mat-label>
<input matInput required min="0" step="1"
formControlName="maxRuleNodeExecutionsPerMessage"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleNodeExecutionsPerMessage').hasError('required')">
{{ 'tenant-profile.max-rule-node-executions-per-message-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleNodeExecutionsPerMessage').hasError('min')">
{{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-emails</mat-label>
<input matInput required min="0" step="1"
formControlName="maxEmails"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxEmails').hasError('required')">
{{ 'tenant-profile.max-emails-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxEmails').hasError('min')">
{{ 'tenant-profile.max-emails-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-sms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSms"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('required')">
{{ 'tenant-profile.max-sms-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('min')">
{{ 'tenant-profile.max-sms-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.max-created-alarms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxCreatedAlarms"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCreatedAlarms').hasError('required')">
{{ 'tenant-profile.max-created-alarms-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCreatedAlarms').hasError('min')">
{{ 'tenant-profile.max-created-alarms-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block"> <fieldset class="fields-group">
<mat-label translate>tenant-profile.transport-tenant-msg-rate-limit</mat-label> <legend class="group-title">
<input matInput formControlName="transportTenantMsgRateLimit"> {{ 'tenant-profile.rule-engine' | translate }} <span translate>tenant-profile.unlimited</span>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantMsgRateLimit').hasError('pattern')"> </legend>
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} <div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
</mat-error> <mat-form-field fxFlex class="mat-block" appearance="fill">
</mat-form-field> <mat-label translate>tenant-profile.max-r-e-executions</mat-label>
<mat-form-field class="mat-block"> <input matInput required min="0" step="1"
<mat-label translate>tenant-profile.transport-tenant-telemetry-msg-rate-limit</mat-label> formControlName="maxREExecutions"
<input matInput formControlName="transportTenantTelemetryMsgRateLimit"> type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryMsgRateLimit').hasError('pattern')"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxREExecutions').hasError('required')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} {{ 'tenant-profile.max-r-e-executions-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxREExecutions').hasError('min')">
<mat-form-field class="mat-block"> {{ 'tenant-profile.max-r-e-executions-range' | translate}}
<mat-label translate>tenant-profile.transport-tenant-telemetry-data-points-rate-limit</mat-label> </mat-error>
<input matInput formControlName="transportTenantTelemetryDataPointsRateLimit"> </mat-form-field>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryDataPointsRateLimit').hasError('pattern')"> <mat-form-field fxFlex class="mat-block" appearance="fill">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} <mat-label translate>tenant-profile.max-transport-messages</mat-label>
</mat-error> <input matInput required min="0" step="1"
</mat-form-field> formControlName="maxTransportMessages"
<mat-form-field class="mat-block"> type="number">
<mat-label translate>tenant-profile.transport-device-msg-rate-limit</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportMessages').hasError('required')">
<input matInput formControlName="transportDeviceMsgRateLimit"> {{ 'tenant-profile.max-transport-messages-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportDeviceMsgRateLimit').hasError('pattern')"> </mat-error>
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportMessages').hasError('min')">
</mat-error> {{ 'tenant-profile.max-transport-messages-required' | translate}}
</mat-form-field> </mat-error>
<mat-form-field class="mat-block"> </mat-form-field>
<mat-label translate>tenant-profile.transport-device-telemetry-msg-rate-limit</mat-label> </div>
<input matInput formControlName="transportDeviceTelemetryMsgRateLimit"> <mat-expansion-panel class="configuration-panel">
</mat-form-field> <mat-expansion-panel-header>
<mat-form-field class="mat-block"> <mat-panel-description fxLayoutAlign="end" translate>
<mat-label translate>tenant-profile.transport-device-telemetry-data-points-rate-limit</mat-label> tenant-profile.advanced-settings
<input matInput formControlName="transportDeviceTelemetryDataPointsRateLimit"> </mat-panel-description>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportDeviceTelemetryDataPointsRateLimit').hasError('pattern')"> </mat-expansion-panel-header>
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} <ng-template matExpansionPanelContent>
</mat-error> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
</mat-form-field> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-form-field class="mat-block"> <mat-label translate>tenant-profile.max-j-s-executions</mat-label>
<mat-label translate>tenant-profile.tenant-rest-limits</mat-label> <input matInput required min="0" step="1"
<input matInput formControlName="maxJSExecutions"
formControlName="tenantServerRestLimitsConfiguration" type="number">
type="text"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxJSExecutions').hasError('required')">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('tenantServerRestLimitsConfiguration').hasError('pattern')"> {{ 'tenant-profile.max-j-s-executions-required' | translate}}
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} </mat-error>
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxJSExecutions').hasError('min')">
</mat-form-field> {{ 'tenant-profile.max-j-s-executions-range' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.customer-rest-limits</mat-label> </mat-form-field>
<input matInput <mat-form-field fxFlex class="mat-block" appearance="fill">
formControlName="customerServerRestLimitsConfiguration" <mat-label translate>tenant-profile.max-transport-data-points</mat-label>
type="text"> <input matInput required min="0" step="1"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('customerServerRestLimitsConfiguration').hasError('pattern')"> formControlName="maxTransportDataPoints"
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} type="number">
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportDataPoints').hasError('required')">
</mat-form-field> {{ 'tenant-profile.max-transport-data-points-required' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-tenant</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxTransportDataPoints').hasError('min')">
<input matInput type="number" formControlName="maxWsSessionsPerTenant"> {{ 'tenant-profile.max-transport-data-points-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerTenant').hasError('min')"> </mat-error>
{{ 'tenant-profile.too-small-value-zero' | translate}} </mat-form-field>
</mat-error> </div>
</mat-form-field> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field class="mat-block"> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-customer</mat-label> <mat-label translate>tenant-profile.max-rule-node-executions-per-message</mat-label>
<input matInput type="number" formControlName="maxWsSessionsPerCustomer"> <input matInput required min="0" step="1"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerCustomer').hasError('min')"> formControlName="maxRuleNodeExecutionsPerMessage"
{{ 'tenant-profile.too-small-value-zero' | translate}} type="number">
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleNodeExecutionsPerMessage').hasError('required')">
</mat-form-field> {{ 'tenant-profile.max-rule-node-executions-per-message-required' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-regular-user</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxRuleNodeExecutionsPerMessage').hasError('min')">
<input matInput type="number" formControlName="maxWsSessionsPerRegularUser"> {{ 'tenant-profile.max-rule-node-executions-per-message-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerRegularUser').hasError('min')"> </mat-error>
{{ 'tenant-profile.too-small-value-zero' | translate}} </mat-form-field>
</mat-error> <div fxFlex></div>
</mat-form-field> </div>
<mat-form-field class="mat-block"> </ng-template>
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-public-user</mat-label> </mat-expansion-panel>
<input matInput type="number" formControlName="maxWsSessionsPerPublicUser"> </fieldset>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerPublicUser').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}} <fieldset class="fields-group">
</mat-error> <legend class="group-title">
</mat-form-field> {{ 'tenant-profile.time-to-live' | translate }} <span translate>tenant-profile.unlimited</span>
<mat-form-field class="mat-block"> </legend>
<mat-label translate>tenant-profile.ws-limit-queue-per-session</mat-label> <div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<input matInput type="number" formControlName="wsMsgQueueLimitPerSession"> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('wsMsgQueueLimitPerSession').hasError('min')"> <mat-label translate>tenant-profile.max-d-p-storage-days</mat-label>
{{ 'tenant-profile.too-small-value-one' | translate}} <input matInput required min="0" step="1"
</mat-error> formControlName="maxDPStorageDays"
</mat-form-field> type="number">
<mat-form-field class="mat-block"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDPStorageDays').hasError('required')">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-tenant</mat-label> {{ 'tenant-profile.max-d-p-storage-days-required' | translate}}
<input matInput type="number" formControlName="maxWsSubscriptionsPerTenant"> </mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerTenant').hasError('min')"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxDPStorageDays').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}} {{ 'tenant-profile.max-d-p-storage-days-range' | translate}}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-customer</mat-label> <mat-label translate>tenant-profile.alarms-ttl-days</mat-label>
<input matInput type="number" formControlName="maxWsSubscriptionsPerCustomer"> <input matInput required min="0" step="1"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerCustomer').hasError('min')"> formControlName="alarmsTtlDays"
{{ 'tenant-profile.too-small-value-zero' | translate}} type="number">
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('required')">
</mat-form-field> {{ 'tenant-profile.alarms-ttl-days-required' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-regular-user</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('min')">
<input matInput type="number" formControlName="maxWsSubscriptionsPerRegularUser"> {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerRegularUser').hasError('min')"> </mat-error>
{{ 'tenant-profile.too-small-value-zero' | translate}} </mat-form-field>
</mat-error> </div>
</mat-form-field> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field class="mat-block"> <mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-public-user</mat-label> <mat-label translate>tenant-profile.default-storage-ttl-days</mat-label>
<input matInput type="number" formControlName="maxWsSubscriptionsPerPublicUser"> <input matInput required min="0" step="1"
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerPublicUser').hasError('min')"> formControlName="defaultStorageTtlDays"
{{ 'tenant-profile.too-small-value-zero' | translate}} type="number">
</mat-error> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('defaultStorageTtlDays').hasError('required')">
</mat-form-field> {{ 'tenant-profile.default-storage-ttl-days-required' | translate}}
<mat-form-field class="mat-block"> </mat-error>
<mat-label translate>tenant-profile.ws-limit-updates-per-session</mat-label> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('defaultStorageTtlDays').hasError('min')">
<input matInput type="text" formControlName="wsUpdatesPerSessionRateLimit"> {{ 'tenant-profile.default-storage-ttl-days-range' | translate}}
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('wsUpdatesPerSessionRateLimit').hasError('pattern')"> </mat-error>
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} </mat-form-field>
</mat-error> <mat-form-field fxFlex class="mat-block" appearance="fill">
</mat-form-field> <mat-label translate>tenant-profile.rpc-ttl-days</mat-label>
<mat-form-field class="mat-block"> <input matInput required min="0" step="1"
<mat-label translate>tenant-profile.cassandra-tenant-limits-configuration</mat-label> formControlName="rpcTtlDays"
<input matInput type="text" formControlName="cassandraQueryTenantRateLimitsConfiguration"> type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('cassandraQueryTenantRateLimitsConfiguration').hasError('pattern')"> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('rpcTtlDays').hasError('required')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}} {{ 'tenant-profile.rpc-ttl-days-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('rpcTtlDays').hasError('min')">
<mat-form-field class="mat-block"> {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}}
<mat-label translate>tenant-profile.tenant-entity-export-rate-limit</mat-label> </mat-error>
<input matInput formControlName="tenantEntityExportRateLimit"> </mat-form-field>
</mat-form-field> </div>
<mat-form-field class="mat-block"> </fieldset>
<mat-label translate>tenant-profile.tenant-entity-import-rate-limit</mat-label>
<input matInput formControlName="tenantEntityImportRateLimit"> <fieldset class="fields-group">
</mat-form-field> <legend class="group-title">
{{ 'tenant-profile.alarms-and-notifications' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.max-emails</mat-label>
<input matInput required min="0" step="1"
formControlName="maxEmails"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxEmails').hasError('required')">
{{ 'tenant-profile.max-emails-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxEmails').hasError('min')">
{{ 'tenant-profile.max-emails-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.max-created-alarms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxCreatedAlarms"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCreatedAlarms').hasError('required')">
{{ 'tenant-profile.max-created-alarms-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxCreatedAlarms').hasError('min')">
{{ 'tenant-profile.max-created-alarms-range' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.max-sms</mat-label>
<input matInput required min="0" step="1"
formControlName="maxSms"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('required')">
{{ 'tenant-profile.max-sms-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxSms').hasError('min')">
{{ 'tenant-profile.max-sms-range' | translate}}
</mat-error>
</mat-form-field>
<div fxFlex></div>
</div>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title">
{{ 'tenant-profile.ota-files-in-bytes' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.maximum-resources-sum-data-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxResourcesInBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('required')">
{{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('min')">
{{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.maximum-ota-packages-sum-data-size</mat-label>
<input matInput required min="0" step="1"
formControlName="maxOtaPackagesInBytes"
type="number">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('required')">
{{ 'tenant-profile.maximum-ota-packages-sum-data-size-required' | translate}}
</mat-error>
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('min')">
{{ 'tenant-profile.maximum-ota-packages-sum-data-size-range' | translate}}
</mat-error>
</mat-form-field>
</div>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title">
{{ 'tenant-profile.ws-title' | translate }} <span translate>tenant-profile.unlimited</span>
</legend>
<div class="fields-element" fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-tenant</mat-label>
<input matInput type="number" formControlName="maxWsSessionsPerTenant">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerTenant').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-tenant</mat-label>
<input matInput type="number" formControlName="maxWsSubscriptionsPerTenant">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerTenant').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-customer</mat-label>
<input matInput type="number" formControlName="maxWsSessionsPerCustomer">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerCustomer').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-customer</mat-label>
<input matInput type="number" formControlName="maxWsSubscriptionsPerCustomer">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerCustomer').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
tenant-profile.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-public-user</mat-label>
<input matInput type="number" formControlName="maxWsSessionsPerPublicUser">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerPublicUser').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-public-user</mat-label>
<input matInput type="number" formControlName="maxWsSubscriptionsPerPublicUser">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerPublicUser').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-sessions-per-regular-user</mat-label>
<input matInput type="number" formControlName="maxWsSessionsPerRegularUser">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSessionsPerRegularUser').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-max-subscriptions-per-regular-user</mat-label>
<input matInput type="number" formControlName="maxWsSubscriptionsPerRegularUser">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxWsSubscriptionsPerRegularUser').hasError('min')">
{{ 'tenant-profile.too-small-value-zero' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block" appearance="fill">
<mat-label translate>tenant-profile.ws-limit-queue-per-session</mat-label>
<input matInput type="number" formControlName="wsMsgQueueLimitPerSession">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('wsMsgQueueLimitPerSession').hasError('min')">
{{ 'tenant-profile.too-small-value-one' | translate}}
</mat-error>
</mat-form-field>
<div fxFlex></div>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title">Rate limits</legend>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-tenant-msg-rate-limit</mat-label>
<input matInput formControlName="transportTenantMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-device-msg-rate-limit</mat-label>
<input matInput formControlName="transportDeviceMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportDeviceMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-tenant-telemetry-msg-rate-limit</mat-label>
<input matInput formControlName="transportTenantTelemetryMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-device-telemetry-msg-rate-limit</mat-label>
<input matInput formControlName="transportDeviceTelemetryMsgRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryMsgRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
</div>
<mat-expansion-panel class="configuration-panel">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
tenant-profile.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-tenant-telemetry-data-points-rate-limit</mat-label>
<input matInput formControlName="transportTenantTelemetryDataPointsRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportTenantTelemetryDataPointsRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.transport-device-telemetry-data-points-rate-limit</mat-label>
<input matInput formControlName="transportDeviceTelemetryDataPointsRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('transportDeviceTelemetryDataPointsRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.tenant-rest-limits</mat-label>
<input matInput
formControlName="tenantServerRestLimitsConfiguration"
type="text">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('tenantServerRestLimitsConfiguration').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.customer-rest-limits</mat-label>
<input matInput
formControlName="customerServerRestLimitsConfiguration"
type="text">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('customerServerRestLimitsConfiguration').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.tenant-entity-export-rate-limit</mat-label>
<input matInput formControlName="tenantEntityExportRateLimit">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.tenant-entity-import-rate-limit</mat-label>
<input matInput formControlName="tenantEntityImportRateLimit">
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.ws-limit-updates-per-session</mat-label>
<input matInput type="text" formControlName="wsUpdatesPerSessionRateLimit">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('wsUpdatesPerSessionRateLimit').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>tenant-profile.cassandra-tenant-limits-configuration</mat-label>
<input matInput type="text" formControlName="cassandraQueryTenantRateLimitsConfiguration">
<mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('cassandraQueryTenantRateLimitsConfiguration').hasError('pattern')">
{{ 'tenant-profile.incorrect-pattern-for-rate-limits' | translate}}
</mat-error>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
</section> </section>

55
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.scss

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.fields-element {
padding-top: 15px;
}
.group-title > span {
color: rgba(0, 0, 0, 0.54);
}
::ng-deep {
.mat-expansion-panel {
&.configuration-panel {
box-shadow: none;
.mat-expansion-panel-header {
padding: 0;
&:hover {
background: none !important;
}
.mat-expansion-indicator {
padding: 2px;
}
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
}
}
}
}

4
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -25,7 +25,7 @@ import { isDefinedAndNotNull } from '@core/utils';
@Component({ @Component({
selector: 'tb-default-tenant-profile-configuration', selector: 'tb-default-tenant-profile-configuration',
templateUrl: './default-tenant-profile-configuration.component.html', templateUrl: './default-tenant-profile-configuration.component.html',
styleUrls: [], styleUrls: ['./default-tenant-profile-configuration.component.scss'],
providers: [{ providers: [{
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultTenantProfileConfigurationComponent), useExisting: forwardRef(() => DefaultTenantProfileConfigurationComponent),
@ -35,7 +35,7 @@ import { isDefinedAndNotNull } from '@core/utils';
export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit { export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit {
defaultTenantProfileConfigurationFormGroup: FormGroup; defaultTenantProfileConfigurationFormGroup: FormGroup;
rateLimitsPattern = '^(((\\d+):(\\d+)),)*((\\d+):(\\d+))$'; rateLimitsPattern = '([1-9]\\d*:[1-9]\\d*)(,[1-9]\\d*:[1-9]\\d*)*';
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {

274
ui-ngx/src/app/modules/home/components/queue/queue-form.component.html

@ -17,7 +17,7 @@
--> -->
<form [formGroup]="queueFormGroup" fxLayout="column"> <form [formGroup]="queueFormGroup" fxLayout="column">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" appearance="fill">
<mat-label translate>admin.queue-name</mat-label> <mat-label translate>admin.queue-name</mat-label>
<input matInput formControlName="name" required> <input matInput formControlName="name" required>
<mat-error *ngIf="queueFormGroup.get('name').hasError('required')"> <mat-error *ngIf="queueFormGroup.get('name').hasError('required')">
@ -27,151 +27,181 @@
{{ 'queue.name-unique' | translate }} {{ 'queue.name-unique' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.poll-interval</mat-label>
<input type="number" matInput formControlName="pollInterval" required>
<mat-error *ngIf="queueFormGroup.get('pollInterval').hasError('required')">
{{ 'queue.poll-interval-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('pollInterval').hasError('min') &&
!queueFormGroup.get('pollInterval').hasError('required')">
{{ 'queue.poll-interval-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.partitions</mat-label>
<input type="number" matInput formControlName="partitions" required>
<mat-error *ngIf="queueFormGroup.get('partitions').hasError('required')">
{{ 'queue.partitions-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('partitions').hasError('min') &&
!queueFormGroup.get('partitions').hasError('required')">
{{ 'queue.partitions-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox class="hinted-checkbox" formControlName="consumerPerPartition">
<div>{{ 'queue.consumer-per-partition' | translate }}</div>
<div class="tb-hint">{{'queue.consumer-per-partition-hint' | translate}}</div>
</mat-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>queue.processing-timeout</mat-label>
<input type="number" matInput formControlName="packProcessingTimeout" required>
<mat-error *ngIf="queueFormGroup.get('packProcessingTimeout').hasError('required')">
{{ 'queue.pack-processing-timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('packProcessingTimeout').hasError('min') &&
!queueFormGroup.get('packProcessingTimeout').hasError('required')">
{{ 'queue.pack-processing-timeout-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-accordion class="queue-strategy" [multi]="true"> <mat-accordion class="queue-strategy" [multi]="true">
<mat-expansion-panel [expanded]="false"> <mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title translate> <mat-panel-title class="panel-title" translate>
queue.submit-strategy queue.submit-settings
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div formGroupName="submitStrategy"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="16px" formGroupName="submitStrategy" style="padding-bottom: 15px;">
<mat-form-field class="mat-block"> <div fxFlex>
<mat-label translate>queue.submit-strategy</mat-label> <label class="group-label" translate>queue.submit-strategy</label>
<mat-select formControlName="type" required> <mat-radio-group fxFlex fxLayout="column" fxLayoutGap="15px" formControlName="type" required>
<mat-option *ngFor="let strategy of submitStrategies" [value]="strategy"> <mat-radio-button *ngFor="let strategy of submitStrategies" [value]="strategy">
{{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }} {{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).label | translate }}
</mat-option> <mat-icon
</mat-select> class="hint-icon"
<mat-error *ngIf="queueFormGroup.get('submitStrategy.type').hasError('required')"> matTooltip="{{ queueSubmitStrategyTypesMap.get(queueSubmitStrategyTypes[strategy]).hint | translate }}"
{{ 'queue.submit-strategy-type-required' | translate }} svgIcon="mdi:information-outline">
</mat-error> </mat-icon>
</mat-form-field> </mat-radio-button>
<mat-form-field class="mat-block" *ngIf="hideBatchSize"> </mat-radio-group>
<mat-label translate>queue.batch-size</mat-label> </div>
<input type="number" matInput formControlName="batchSize" required> <div fxFlex *ngIf="hideBatchSize">
<mat-error *ngIf="queueFormGroup.get('submitStrategy.batchSize').hasError('required')"> <label class="group-label" translate>queue.grouping-parameter</label>
{{ 'queue.batch-size-required' | translate }} <mat-form-field class="mat-block">
</mat-error> <mat-label translate>queue.batch-size</mat-label>
<mat-error *ngIf="queueFormGroup.get('submitStrategy.batchSize').hasError('min') && <input type="number" matInput formControlName="batchSize" required>
!queueFormGroup.get('submitStrategy.batchSize').hasError('required')"> <mat-error *ngIf="queueFormGroup.get('submitStrategy.batchSize').hasError('required')">
{{ 'queue.batch-size-min-value' | translate }} {{ 'queue.batch-size-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> <mat-error *ngIf="queueFormGroup.get('submitStrategy.batchSize').hasError('min') &&
!queueFormGroup.get('submitStrategy.batchSize').hasError('required')">
{{ 'queue.batch-size-min-value' | translate }}
</mat-error>
</mat-form-field>
</div>
</div> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel [expanded]="false"> <mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title translate> <mat-panel-title class="panel-title" translate>
queue.processing-strategy queue.processing-settings
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div formGroupName="processingStrategy"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="16px" formGroupName="processingStrategy" style="padding-bottom: 15px;">
<mat-form-field class="mat-block"> <div fxFlex>
<mat-label translate>queue.processing-strategy</mat-label> <label class="group-label" translate>queue.processing-strategy</label>
<mat-select formControlName="type" required> <mat-radio-group fxFlex fxLayout="column" fxLayoutGap="15px" formControlName="type" required>
<mat-option *ngFor="let strategy of processingStrategies" [value]="strategy"> <mat-radio-button *ngFor="let strategy of processingStrategies" [value]="strategy">
{{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).label | translate }} {{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).label | translate }}
</mat-option> <mat-icon
</mat-select> class="hint-icon"
<mat-error *ngIf="queueFormGroup.get('processingStrategy.type').hasError('required')"> matTooltip="{{ queueProcessingStrategyTypesMap.get(queueProcessingStrategyTypes[strategy]).hint | translate }}"
{{ 'queue.processing-strategy-type-required' | translate }} svgIcon="mdi:information-outline">
</mat-error> </mat-icon>
</mat-form-field> </mat-radio-button>
<mat-form-field class="mat-block"> </mat-radio-group>
<mat-label translate>queue.retries</mat-label> </div>
<input type="number" matInput formControlName="retries" required> <div fxFlex>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.retries').hasError('required')"> <label class="group-label" translate>queue.retries-settings</label>
{{ 'queue.retries-required' | translate }} <mat-form-field class="mat-block">
</mat-error> <mat-label translate>queue.retries</mat-label>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.retries').hasError('min') && <input type="number" matInput formControlName="retries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.retries').hasError('required')">
{{ 'queue.retries-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.retries').hasError('min') &&
!queueFormGroup.get('processingStrategy.retries').hasError('required')"> !queueFormGroup.get('processingStrategy.retries').hasError('required')">
{{ 'queue.retries-min-value' | translate }} {{ 'queue.retries-min-value' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>queue.failure-percentage</mat-label> <mat-label translate>queue.failure-percentage</mat-label>
<input type="number" matInput formControlName="failurePercentage" required> <input type="number" matInput formControlName="failurePercentage" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('required')"> <mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('required')">
{{ 'queue.failure-percentage-required' | translate }} {{ 'queue.failure-percentage-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('min') && <mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('min') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') && !queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('max')"> !queueFormGroup.get('processingStrategy.failurePercentage').hasError('max')">
{{ 'queue.failure-percentage-min-value' | translate }} {{ 'queue.failure-percentage-min-value' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('max') && <mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('max') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') && !queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('min')"> !queueFormGroup.get('processingStrategy.failurePercentage').hasError('min')">
{{ 'queue.failure-percentage-max-value' | translate }} {{ 'queue.failure-percentage-max-value' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>queue.pause-between-retries</mat-label> <mat-label translate>queue.pause-between-retries</mat-label>
<input type="number" matInput formControlName="pauseBetweenRetries" required> <input type="number" matInput formControlName="pauseBetweenRetries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')"> <mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')">
{{ 'queue.pause-between-retries-required' | translate }} {{ 'queue.pause-between-retries-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('min') && <mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('min') &&
!queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')"> !queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')">
{{ 'queue.pause-between-retries-min-value' | translate }} {{ 'queue.pause-between-retries-min-value' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>queue.max-pause-between-retries</mat-label> <mat-label translate>queue.max-pause-between-retries</mat-label>
<input type="number" matInput formControlName="maxPauseBetweenRetries" required> <input type="number" matInput formControlName="maxPauseBetweenRetries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')"> <mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')">
{{ 'queue.max-pause-between-retries-required' | translate }} {{ 'queue.max-pause-between-retries-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('min') && <mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('min') &&
!queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')"> !queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')">
{{ 'queue.max-pause-between-retries-min-value' | translate }} {{ 'queue.max-pause-between-retries-min-value' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div>
</div>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title class="panel-title" translate>
queue.polling-settings
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="column" fxLayoutGap="10px" style="padding-bottom: 15px;">
<div fxFlex>
<label class="group-label" translate>queue.batch-processing</label>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="10px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>queue.poll-interval</mat-label>
<input type="number" matInput formControlName="pollInterval" required>
<mat-error *ngIf="queueFormGroup.get('pollInterval').hasError('required')">
{{ 'queue.poll-interval-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('pollInterval').hasError('min') &&
!queueFormGroup.get('pollInterval').hasError('required')">
{{ 'queue.poll-interval-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>queue.partitions</mat-label>
<input type="number" matInput formControlName="partitions" required>
<mat-error *ngIf="queueFormGroup.get('partitions').hasError('required')">
{{ 'queue.partitions-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('partitions').hasError('min') &&
!queueFormGroup.get('partitions').hasError('required')">
{{ 'queue.partitions-min-value' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<div fxFlex>
<label class="group-label" translate>queue.immediate-processing</label>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="10px">
<mat-checkbox fxFlex fxFlexAlign.gt-xs="center" formControlName="consumerPerPartition">
{{ 'queue.consumer-per-partition' | translate }}
</mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>queue.processing-timeout</mat-label>
<input type="number" matInput formControlName="packProcessingTimeout" required>
<mat-error *ngIf="queueFormGroup.get('packProcessingTimeout').hasError('required')">
{{ 'queue.pack-processing-timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('packProcessingTimeout').hasError('min') &&
!queueFormGroup.get('packProcessingTimeout').hasError('required')">
{{ 'queue.pack-processing-timeout-min-value' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>
<mat-form-field class="mat-block" formGroupName="additionalInfo"> <mat-form-field class="mat-block" formGroupName="additionalInfo" appearance="fill">
<mat-label translate>queue.description</mat-label> <mat-label translate>queue.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea> <textarea matInput formControlName="description" rows="2"></textarea>
<mat-hint translate>queue.description-hint</mat-hint> <mat-hint translate>queue.description-hint</mat-hint>

32
ui-ngx/src/app/modules/home/components/queue/queue-form.component.scss

@ -13,16 +13,38 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { :host {
.queue-strategy { .queue-strategy {
padding-bottom: 16px; padding-bottom: 16px;
.mat-expansion-panel-header { .group-label {
height: 50px; display: block;
padding-bottom: 10px;
} }
.mat-expansion-panel-body { .panel-title {
padding-bottom: 0 !important; font-weight: 500;
}
.hint-icon {
width: 15px;
height: 15px;
vertical-align: top;
}
}
::ng-deep {
.mat-form-field-infix {
width: 100% !important;
}
.queue-strategy {
.mat-expansion-panel-header {
height: 50px;
}
.mat-expansion-panel-body {
padding-bottom: 0 !important;
}
} }
} }
} }

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

@ -2959,18 +2959,25 @@
"add" : "Add queue", "add" : "Add queue",
"details": "Queue details", "details": "Queue details",
"topic": "Topic", "topic": "Topic",
"submit-strategy": "Submit Strategy", "submit-settings": "Submit settings",
"processing-strategy": "Processing Strategy", "submit-strategy": "Strategy type *",
"grouping-parameter": "Grouping parameter",
"processing-settings": "Retries processing settings",
"processing-strategy": "Processing type *",
"retries-settings": "Retries settings",
"polling-settings": "Polling settings",
"batch-processing": "Batch processing",
"poll-interval": "Poll interval", "poll-interval": "Poll interval",
"partitions": "Partitions", "partitions": "Partitions",
"consumer-per-partition": "Consumer per partition", "immediate-processing": "Immediate processing",
"consumer-per-partition": "Send message poll for each consumer",
"consumer-per-partition-hint": "Enable separate consumer(s) per each partition", "consumer-per-partition-hint": "Enable separate consumer(s) per each partition",
"processing-timeout": "Processing timeout, ms", "processing-timeout": "Processing within, ms",
"batch-size": "Batch size", "batch-size": "Batch size",
"retries": "Retries (0 - unlimited)", "retries": "Number of retries (0 – unlimited)",
"failure-percentage": "Failure Percentage", "failure-percentage": "Percentage of failure messages for skipping retries",
"pause-between-retries": "Pause between retries", "pause-between-retries": "Retry within, sec",
"max-pause-between-retries": "Maximal pause between retries", "max-pause-between-retries": "Additional retry within, sec",
"delete": "Delete queue", "delete": "Delete queue",
"copyId": "Copy queue Id", "copyId": "Copy queue Id",
"idCopiedMessage": "Queue Id has been copied to clipboard", "idCopiedMessage": "Queue Id has been copied to clipboard",
@ -3085,93 +3092,101 @@
"export-failed-error": "Unable to export tenant profile: {{error}}", "export-failed-error": "Unable to export tenant profile: {{error}}",
"tenant-profile-file": "Tenant profile file", "tenant-profile-file": "Tenant profile file",
"invalid-tenant-profile-file-error": "Unable to import tenant profile: Invalid tenant profile data structure.", "invalid-tenant-profile-file-error": "Unable to import tenant profile: Invalid tenant profile data structure.",
"maximum-devices": "Maximum number of devices (0 - unlimited)", "advanced-settings": "Advanced settings",
"maximum-devices-required": "Maximum number of devices is required.", "entities": "Entities",
"maximum-devices-range": "Minimum number of devices can't be negative", "rule-engine": "Rule Engine",
"maximum-assets": "Maximum number of assets (0 - unlimited)", "time-to-live": "Time-to-live",
"maximum-assets-required": "Maximum number of assets is required.", "alarms-and-notifications": "Alarms and notifications",
"maximum-assets-range": "Maximum number of assets can't be negative", "ota-files-in-bytes": "OTA files in bytes",
"maximum-customers": "Maximum number of customers (0 - unlimited)", "ws-title": "WS",
"maximum-customers-required": "Maximum number of customers is required.", "unlimited": "(0 - unlimited)",
"maximum-customers-range": "Maximum number of customers can't be negative", "maximum-devices": "Devices maximum number",
"maximum-users": "Maximum number of users (0 - unlimited)", "maximum-devices-required": "Devices maximum number is required.",
"maximum-users-required": "Maximum number of users is required.", "maximum-devices-range": "Devices maximum number can't be negative",
"maximum-users-range": "Maximum number of users can't be negative", "maximum-assets": "Assets maximum number",
"maximum-dashboards": "Maximum number of dashboards (0 - unlimited)", "maximum-assets-required": "Assets maximum number is required.",
"maximum-dashboards-required": "Maximum number of dashboards is required.", "maximum-assets-range": "Assets maximum number can't be negative",
"maximum-dashboards-range": "Maximum number of dashboards can't be negative", "maximum-customers": "Customers maximum number",
"maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)", "maximum-customers-required": "Customers maximum number is required.",
"maximum-rule-chains-required": "Maximum number of rule chains is required.", "maximum-customers-range": "Customers maximum number can't be negative",
"maximum-rule-chains-range": "Maximum number of rule chains can't be negative", "maximum-users": "Users maximum number",
"maximum-resources-sum-data-size": "Maximum sum of resource files size in bytes (0 - unlimited)", "maximum-users-required": "Users maximum number is required.",
"maximum-resources-sum-data-size-required": "Maximum sum of resource files size is required.", "maximum-users-range": "Users maximum number can't be negative",
"maximum-resources-sum-data-size-range": "Maximum sum of resource files size can`t be negative", "maximum-dashboards": "Dashboards maximum number",
"maximum-ota-packages-sum-data-size": "Maximum sum of ota package files size in bytes (0 - unlimited)", "maximum-dashboards-required": "Dashboards maximum number is required.",
"maximum-ota-package-sum-data-size-required": "Maximum sum of ota package files size is required.", "maximum-dashboards-range": "Dashboards maximum number can't be negative",
"maximum-ota-package-sum-data-size-range": "Maximum sum of ota package files size can`t be negative", "maximum-rule-chains": "Rule chains maximum number",
"transport-tenant-msg-rate-limit": "Transport tenant messages rate limit.", "maximum-rule-chains-required": "Rule chains maximum number is required.",
"transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.", "maximum-rule-chains-range": "Rule chains maximum number can't be negative",
"transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.", "maximum-resources-sum-data-size": "Resource files sum size",
"transport-device-msg-rate-limit": "Transport device messages rate limit.", "maximum-resources-sum-data-size-required": "Resource files sum size is required.",
"transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages rate limit.", "maximum-resources-sum-data-size-range": "Resource files sum size can`t be negative",
"transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points rate limit.", "maximum-ota-packages-sum-data-size": "OTA package files sum size",
"tenant-entity-export-rate-limit": "Entity version creation rate limit", "maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.",
"tenant-entity-import-rate-limit": "Entity version load rate limit", "maximum-ota-package-sum-data-size-range": "OTA package files sum size can`t be negative",
"max-transport-messages": "Maximum number of transport messages (0 - unlimited)", "transport-tenant-msg-rate-limit": "Transport tenant messages",
"max-transport-messages-required": "Maximum number of transport messages is required.", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages",
"max-transport-messages-range": "Maximum number of transport messages can't be negative", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points",
"max-transport-data-points": "Maximum number of transport data points (0 - unlimited)", "transport-device-msg-rate-limit": "Transport device messages",
"max-transport-data-points-required": "Maximum number of transport data points is required.", "transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages",
"max-transport-data-points-range": "Maximum number of transport data points can't be negative", "transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points",
"max-r-e-executions": "Maximum number of Rule Engine executions (0 - unlimited)", "tenant-entity-export-rate-limit": "Entity version creation",
"max-r-e-executions-required": "Maximum number of Rule Engine executions is required.", "tenant-entity-import-rate-limit": "Entity version load",
"max-r-e-executions-range": "Maximum number of Rule Engine executions can't be negative", "max-transport-messages": "Transport messages maximum number",
"max-j-s-executions": "Maximum number of JavaScript executions (0 - unlimited)", "max-transport-messages-required": "Transport messages maximum number is required.",
"max-j-s-executions-required": "Maximum number of JavaScript executions is required.", "max-transport-messages-range": "Transport messages maximum number can't be negative",
"max-j-s-executions-range": "Maximum number of JavaScript executions can't be negative", "max-transport-data-points": "Transport data points maximum number ",
"max-d-p-storage-days": "Maximum number of data points storage days (0 - unlimited)", "max-transport-data-points-required": "Transport data points maximum number is required.",
"max-d-p-storage-days-required": "Maximum number of data points storage days is required.", "max-transport-data-points-range": "Transport data points maximum number can't be negative",
"max-d-p-storage-days-range": "Maximum number of data points storage days can't be negative", "max-r-e-executions": "Rule Engine executions maximum number",
"default-storage-ttl-days": "Default storage TTL days (0 - unlimited)", "max-r-e-executions-required": "Rule Engine executions maximum number is required.",
"default-storage-ttl-days-required": "Default storage TTL days is required.", "max-r-e-executions-range": "Rule Engine executions maximum number can't be negative",
"default-storage-ttl-days-range": "Default storage TTL days can't be negative", "max-j-s-executions": "JavaScript executions maximum number ",
"alarms-ttl-days": "Alarms TTL days (0 - unlimited)", "max-j-s-executions-required": "JavaScript executions maximum number is required.",
"max-j-s-executions-range": "JavaScript executions maximum number can't be negative",
"max-d-p-storage-days": "Data points storage days maximum number",
"max-d-p-storage-days-required": "Data points storage days maximum number is required.",
"max-d-p-storage-days-range": "Data points storage days maximum number can't be negative",
"default-storage-ttl-days": "Storage TTL days by default",
"default-storage-ttl-days-required": "Storage TTL days by default is required.",
"default-storage-ttl-days-range": "Storage TTL days by default can't be negative",
"alarms-ttl-days": "Alarms TTL days",
"alarms-ttl-days-required": "Alarms TTL days required", "alarms-ttl-days-required": "Alarms TTL days required",
"alarms-ttl-days-days-range": "Alarms TTL days can't be negative", "alarms-ttl-days-days-range": "Alarms TTL days can't be negative",
"rpc-ttl-days": "RPC TTL days (0 - unlimited)", "rpc-ttl-days": "RPC TTL days",
"rpc-ttl-days-required": "RPC TTL days required", "rpc-ttl-days-required": "RPC TTL days required",
"rpc-ttl-days-days-range": "RPC TTL days can't be negative", "rpc-ttl-days-days-range": "RPC TTL days can't be negative",
"max-rule-node-executions-per-message": "Maximum number of rule node executions per message (0 - unlimited)", "max-rule-node-executions-per-message": "Rule node per message executions maximum number",
"max-rule-node-executions-per-message-required": "Maximum number of rule node executions per message is required.", "max-rule-node-executions-per-message-required": "MRule node per message executions maximum number is required.",
"max-rule-node-executions-per-message-range": "Maximum number of rule node executions per message can't be negative", "max-rule-node-executions-per-message-range": "Rule node per message executions maximum number can't be negative",
"max-emails": "Maximum number of emails sent (0 - unlimited)", "max-emails": "Emails sent maximum number",
"max-emails-required": "Maximum number of emails sent is required.", "max-emails-required": "Emails sent maximum number is required.",
"max-emails-range": "Maximum number of emails sent can't be negative", "max-emails-range": "Emails sent maximum number can't be negative",
"max-sms": "Maximum number of SMS sent (0 - unlimited)", "max-sms": "SMS sent maximum number",
"max-sms-required": "Maximum number of SMS sent is required.", "max-sms-required": "SMS sent maximum number is required.",
"max-sms-range": "Maximum number of SMS sent can't be negative", "max-sms-range": "SMS sent maximum number can't be negative",
"max-created-alarms": "Maximum number of alarms created (0 - unlimited)", "max-created-alarms": "Alarms created maximum number",
"max-created-alarms-required": "Maximum number of alarms created is required.", "max-created-alarms-required": "Alarms created maximum number is required.",
"max-created-alarms-range": "Maximum number of alarms created can't be negative", "max-created-alarms-range": "Alarms created maximum number be negative",
"no-queue": "No Queue configured", "no-queue": "No Queue configured",
"add-queue": "Add Queue", "add-queue": "Add Queue",
"queues-with-count": "Queues ({{count}})", "queues-with-count": "Queues ({{count}})",
"tenant-rest-limits": "Rate limit for REST requests for tenant", "tenant-rest-limits": "REST requests for tenant",
"customer-rest-limits": "Rate limit for REST requests for customer", "customer-rest-limits": "REST requests for customer",
"incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60",
"too-small-value-zero": "The value must be bigger than 0", "too-small-value-zero": "The value must be bigger than 0",
"too-small-value-one": "The value must be bigger than 1", "too-small-value-one": "The value must be bigger than 1",
"cassandra-tenant-limits-configuration": "Cassandra query rate limit for tenant", "cassandra-tenant-limits-configuration": "Cassandra query for tenant",
"ws-limit-max-sessions-per-tenant": "Maximum number of WS sessions per tenant", "ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number",
"ws-limit-max-sessions-per-customer": "Maximum number of WS sessions per customer", "ws-limit-max-sessions-per-customer": "Sessions per customer maximum number",
"ws-limit-max-sessions-per-regular-user": "Maximum number of WS sessions per regular user", "ws-limit-max-sessions-per-regular-user": "Sessions per regular user maximum number",
"ws-limit-max-sessions-per-public-user": "Maximum number of WS sessions per public user", "ws-limit-max-sessions-per-public-user": "Sessions per public user maximum number",
"ws-limit-queue-per-session": "Maximum size of WS message queue per session", "ws-limit-queue-per-session": "Message queue per session maximum size",
"ws-limit-max-subscriptions-per-tenant": "Maximum number of WS subscriptions per tenant", "ws-limit-max-subscriptions-per-tenant": "Subscriptions per tenant maximum number",
"ws-limit-max-subscriptions-per-customer": "Maximum number of WS subscriptions per customer", "ws-limit-max-subscriptions-per-customer": "Subscriptions per customer maximum number",
"ws-limit-max-subscriptions-per-regular-user": "Maximum number of WS subscriptions per regular user", "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number",
"ws-limit-max-subscriptions-per-public-user": "Maximum number of WS subscriptions per public user", "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number",
"ws-limit-updates-per-session": "Rate limit for WS updates per session" "ws-limit-updates-per-session": "WS updates per session"
}, },
"timeinterval": { "timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",

Loading…
Cancel
Save