Browse Source

Merge pull request #6616 from ViacheslavKlimov/vc-improvements

VC improvements
pull/6641/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
8f9a7e0401
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      application/src/main/java/org/thingsboard/server/controller/QueueController.java
  2. 76
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
  3. 26
      application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
  4. 56
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
  6. 18
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
  8. 39
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  9. 28
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  10. 10
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
  11. 77
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  12. 191
      application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java
  13. 1
      common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java
  14. 2
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportResult.java
  15. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java
  16. 5
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileQueueConfiguration.java
  18. 8
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java
  19. 6
      dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java
  20. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java
  21. 100
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  22. 37
      ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html
  23. 12
      ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts
  24. 16
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts
  25. 4
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.scss
  26. 7
      ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts
  27. 8
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  28. 2
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  29. 337
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.html
  30. 37
      ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts
  31. 18
      ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts
  32. 9
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html
  33. 29
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss
  34. 7
      ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts
  35. 1
      ui-ngx/src/app/shared/models/queue.models.ts
  36. 5
      ui-ngx/src/app/shared/models/tenant.model.ts
  37. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

31
application/src/main/java/org/thingsboard/server/controller/QueueController.java

@ -15,10 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@ -38,14 +35,7 @@ import org.thingsboard.server.service.entitiy.queue.TbQueueService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@ -55,27 +45,6 @@ public class QueueController extends BaseController {
private final TbQueueService tbQueueService;
@ApiOperation(value = "Get queue names (getTenantQueuesByServiceType)",
notes = "Returns a set of unique queue names based on service type. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/queues", params = {"serviceType"}, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
@ResponseBody()
public Set<String> getTenantQueuesByServiceType(@ApiParam(value = QUEUE_SERVICE_TYPE_DESCRIPTION, allowableValues = QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES)
@RequestParam String serviceType) throws ThingsboardException {
checkParameter("serviceType", serviceType);
try {
ServiceType type = ServiceType.valueOf(serviceType);
switch (type) {
case TB_RULE_ENGINE:
return queueService.findQueuesByTenantId(getTenantId()).stream().map(Queue::getName).collect(Collectors.toSet());
default:
return Collections.emptySet();
}
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody

76
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java

@ -0,0 +1,76 @@
/**
* 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.service.apiusage;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
public class DefaultRateLimitService implements RateLimitService {
private final TbTenantProfileCache tenantProfileCache;
private final Map<String, Map<TenantId, TbRateLimits>> rateLimits = new ConcurrentHashMap<>();
@Override
public boolean checkEntityExportLimit(TenantId tenantId) {
return checkLimit(tenantId, "entityExport", DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit);
}
@Override
public boolean checkEntityImportLimit(TenantId tenantId) {
return checkLimit(tenantId, "entityImport", DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit);
}
private boolean checkLimit(TenantId tenantId, String rateLimitsKey, Function<DefaultTenantProfileConfiguration, String> rateLimitConfigExtractor) {
String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration()
.map(rateLimitConfigExtractor).orElse(null);
Map<TenantId, TbRateLimits> rateLimits = this.rateLimits.get(rateLimitsKey);
if (StringUtils.isEmpty(rateLimitConfig)) {
if (rateLimits != null) {
rateLimits.remove(tenantId);
if (rateLimits.isEmpty()) {
this.rateLimits.remove(rateLimitsKey);
}
}
return true;
}
if (rateLimits == null) {
rateLimits = new ConcurrentHashMap<>();
this.rateLimits.put(rateLimitsKey, rateLimits);
}
TbRateLimits rateLimit = rateLimits.get(tenantId);
if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {
rateLimit = new TbRateLimits(rateLimitConfig);
rateLimits.put(tenantId, rateLimit);
}
return rateLimit.tryConsume();
}
}

26
application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java

@ -0,0 +1,26 @@
/**
* 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.service.apiusage;
import org.thingsboard.server.common.data.id.TenantId;
public interface RateLimitService {
boolean checkEntityExportLimit(TenantId tenantId);
boolean checkEntityImportLimit(TenantId tenantId);
}

56
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -19,42 +19,42 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.sync.ThrowingRunnable;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.RateLimitService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService;
import org.thingsboard.server.service.sync.ie.exporting.impl.DefaultEntityExportService;
import org.thingsboard.server.service.sync.ie.importing.EntityImportService;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
@TbCoreComponent
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings("rawtypes")
public class DefaultEntitiesExportImportService implements EntitiesExportImportService {
private final Map<EntityType, EntityExportService<?, ?, ?>> exportServices = new HashMap<>();
private final Map<EntityType, EntityImportService<?, ?, ?>> importServices = new HashMap<>();
private final RateLimitService rateLimitService;
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.ASSET, EntityType.RULE_CHAIN,
EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE
@ -63,16 +63,22 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(SecurityUser user, I entityId, EntityExportSettings exportSettings) throws ThingsboardException {
if (!rateLimitService.checkEntityExportLimit(user.getTenantId())) {
throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
EntityType entityType = entityId.getEntityType();
EntityExportService<I, E, EntityExportData<E>> exportService = getExportService(entityType);
return exportService.getExportData(user, entityId, exportSettings);
}
@Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(SecurityUser user, EntityExportData<E> exportData, EntityImportSettings importSettings,
boolean saveReferences, boolean sendEvents) throws ThingsboardException {
if (!rateLimitService.checkEntityImportLimit(user.getTenantId())) {
throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
if (exportData.getEntity() == null || exportData.getEntity().getId() == null) {
throw new DataValidationException("Invalid entity data");
}
@ -92,44 +98,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
return importResult;
}
@Transactional(rollbackFor = Exception.class, timeout = 120)
@Override
public List<EntityImportResult<?>> importEntities(SecurityUser user, List<EntityExportData<?>> exportDataList, EntityImportSettings importSettings) throws ThingsboardException {
exportDataList.sort(getDataComparatorForImport());
List<EntityImportResult<?>> importResults = new ArrayList<>();
for (EntityExportData exportData : exportDataList) {
EntityImportResult<?> importResult = importEntity(user, exportData, importSettings, false, false);
importResults.add(importResult);
}
for (ThrowingRunnable saveReferencesCallback : importResults.stream()
.map(EntityImportResult::getSaveReferencesCallback)
.filter(Objects::nonNull)
.collect(Collectors.toList())) {
saveReferencesCallback.run();
}
importResults.stream()
.map(EntityImportResult::getSendEventsCallback)
.filter(Objects::nonNull)
.forEach(sendEventsCallback -> {
try {
sendEventsCallback.run();
} catch (Exception e) {
log.error("Failed to send event for entity", e);
}
});
return importResults;
}
@Override
public Comparator<EntityExportData<?>> getDataComparatorForImport() {
return Comparator.comparing(EntityExportData::getEntityType, getEntityTypeComparatorForImport());
}
@Override
public Comparator<EntityType> getEntityTypeComparatorForImport() {

7
application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java

@ -19,14 +19,13 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Comparator;
import java.util.List;
public interface EntitiesExportImportService {
@ -35,10 +34,6 @@ public interface EntitiesExportImportService {
<E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(SecurityUser user, EntityExportData<E> exportData, EntityImportSettings importSettings,
boolean saveReferences, boolean sendEvents) throws ThingsboardException;
List<EntityImportResult<?>> importEntities(SecurityUser user, List<EntityExportData<?>> exportDataList, EntityImportSettings importSettings) throws ThingsboardException;
Comparator<EntityExportData<?>> getDataComparatorForImport();
Comparator<EntityType> getEntityTypeComparatorForImport();

18
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java

@ -87,19 +87,23 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi
@Override
public <E extends HasId<I>, I extends EntityId> E findEntityByTenantIdAndId(TenantId tenantId, I id) {
E entity = findEntityById(id);
if (entity == null || !belongsToTenant(entity, tenantId)) {
return null;
}
return entity;
}
@Override
public <E extends HasId<I>, I extends EntityId> E findEntityById(I id) {
EntityType entityType = id.getEntityType();
Dao<E> dao = getDao(entityType);
if (dao == null) {
throw new IllegalArgumentException("Unsupported entity type " + entityType);
}
E entity = dao.findById(tenantId, id.getId());
if (entity == null || !belongsToTenant(entity, tenantId)) {
return null;
}
return entity;
return dao.findById(TenantId.SYS_TENANT_ID, id.getId());
}
@Override

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java

@ -32,6 +32,8 @@ public interface ExportableEntitiesService {
<E extends HasId<I>, I extends EntityId> E findEntityByTenantIdAndId(TenantId tenantId, I id);
<E extends HasId<I>, I extends EntityId> E findEntityById(I id);
<E extends ExportableEntity<I>, I extends EntityId> E findEntityByTenantIdAndName(TenantId tenantId, EntityType entityType, String name);
<E extends ExportableEntity<I>, I extends EntityId> PageData<E> findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);

39
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -84,7 +84,8 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
entity.setExternalId(entity.getId());
IdProvider idProvider = new IdProvider(user);
EntityImportResult<E> importResult = new EntityImportResult<>();
IdProvider idProvider = new IdProvider(user, importSettings, importResult);
setOwner(user.getTenantId(), entity, idProvider);
if (existingEntity == null) {
entity.setId(null);
@ -97,7 +98,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
E savedEntity = prepareAndSave(user.getTenantId(), entity, exportData, idProvider, importSettings);
EntityImportResult<E> importResult = new EntityImportResult<>();
importResult.setSavedEntity(savedEntity);
importResult.setOldEntity(existingEntity);
importResult.setEntityType(getEntityType());
@ -255,11 +255,27 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@RequiredArgsConstructor
protected class IdProvider {
private final SecurityUser user;
private final EntityImportSettings importSettings;
private final EntityImportResult<E> importResult;
public <ID extends EntityId> ID getInternalId(ID externalId) {
return getInternalId(externalId, true);
}
public <ID extends EntityId> ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) {
if (externalId == null || externalId.isNullUid()) return null;
HasId<ID> entity = findInternalEntity(user.getTenantId(), externalId);
HasId<ID> entity;
try {
entity = findInternalEntity(user.getTenantId(), externalId);
} catch (Exception e) {
if (throwExceptionIfNotFound) {
throw e;
} else {
importResult.setUpdatedAllExternalIds(false);
return null;
}
}
try {
exportableEntitiesService.checkPermission(user, entity, entity.getId().getEntityType(), Operation.READ);
} catch (ThingsboardException e) {
@ -269,6 +285,8 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
public Optional<EntityId> getInternalIdByUuid(UUID externalUuid) {
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
for (EntityType entityType : EntityType.values()) {
EntityId externalId;
try {
@ -277,16 +295,19 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
continue;
}
EntityId internalId = null;
try {
internalId = getInternalId(externalId);
} catch (Exception ignored) {
}
EntityId internalId = getInternalId(externalId, false);
if (internalId != null) {
return Optional.of(internalId);
} else if (importSettings.isResetExternalIdsOfAnotherTenant()) {
try {
if (exportableEntitiesService.findEntityById(externalId) != null) {
return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, EntityId.NULL_UUID));
}
} catch (Exception ignored) {}
}
}
importResult.setUpdatedAllExternalIds(false);
return Optional.empty();
}

28
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java

@ -16,6 +16,8 @@
package org.thingsboard.server.service.sync.ie.importing.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
@ -27,13 +29,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.utils.RegexUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
@ -66,12 +69,21 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
@Override
protected Dashboard prepareAndSave(TenantId tenantId, Dashboard dashboard, EntityExportData<Dashboard> exportData, IdProvider idProvider, EntityImportSettings importSettings) {
JsonNode configuration = dashboard.getConfiguration();
String newConfigurationJson = RegexUtils.replace(configuration.toString(), RegexUtils.UUID_PATTERN, uuid -> {
return idProvider.getInternalIdByUuid(UUID.fromString(uuid))
.map(entityId -> entityId.getId().toString()).orElse(uuid);
});
configuration = JacksonUtil.toJsonNode(newConfigurationJson);
dashboard.setConfiguration(configuration);
JsonNode entityAliases = configuration.get("entityAliases");
if (entityAliases != null && entityAliases.isObject()) {
for (JsonNode entityAlias : entityAliases) {
ArrayList<String> fields = Lists.newArrayList(entityAlias.fieldNames());
for (String field : fields) {
if (field.equals("id")) continue;
JsonNode oldFieldValue = entityAlias.get(field);
JsonNode newFieldValue = JacksonUtil.toJsonNode(RegexUtils.replace(oldFieldValue.toString(), RegexUtils.UUID_PATTERN, uuid -> {
return idProvider.getInternalIdByUuid(UUID.fromString(uuid))
.map(entityId -> entityId.getId().toString()).orElse(uuid);
}));
((ObjectNode) entityAlias).set(field, newFieldValue);
}
}
}
Set<ShortCustomerInfo> assignedCustomers = Optional.ofNullable(dashboard.getAssignedCustomers()).orElse(Collections.emptySet()).stream()
.peek(customerInfo -> customerInfo.setCustomerId(idProvider.getInternalId(customerInfo.getCustomerId())))

10
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java

@ -28,12 +28,11 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.utils.RegexUtils;
import java.util.Collections;
@ -80,14 +79,13 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
});
Optional.ofNullable(metaData.getRuleChainConnections()).orElse(Collections.emptyList())
.forEach(ruleChainConnectionInfo -> {
ruleChainConnectionInfo.setTargetRuleChainId(idProvider.getInternalId(ruleChainConnectionInfo.getTargetRuleChainId()));
ruleChainConnectionInfo.setTargetRuleChainId(idProvider.getInternalId(ruleChainConnectionInfo.getTargetRuleChainId(), false));
});
ruleChain.setFirstRuleNodeId(null);
ruleChain = ruleChainService.saveRuleChain(ruleChain);
exportData.getMetaData().setRuleChainId(ruleChain.getId());
RuleChainUpdateResult updateResult = ruleChainService.saveRuleChainMetaData(tenantId, exportData.getMetaData());
// FIXME [viacheslav]: send events for nodes
ruleChainService.saveRuleChainMetaData(tenantId, exportData.getMetaData());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}

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

@ -22,7 +22,6 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionCallback;
@ -46,13 +45,13 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
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.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
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.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.request.create.AutoVersionCreateConfig;
@ -257,16 +256,17 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
}
private VersionLoadResult loadMultipleEntities(SecurityUser user, EntityTypeVersionLoadRequest versionLoadRequest) {
private VersionLoadResult loadMultipleEntities(SecurityUser user, EntityTypeVersionLoadRequest request) {
Map<EntityType, EntityTypeLoadResult> results = new HashMap<>();
Map<EntityType, Set<EntityId>> importedEntities = new HashMap<>();
Map<EntityId, EntityImportSettings> toReimport = new HashMap<>();
List<ThrowingRunnable> saveReferencesCallbacks = new ArrayList<>();
List<ThrowingRunnable> sendEventsCallbacks = new ArrayList<>();
List<EntityType> entityTypes = versionLoadRequest.getEntityTypes().keySet().stream()
List<EntityType> entityTypes = request.getEntityTypes().keySet().stream()
.sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList());
for (EntityType entityType : entityTypes) {
EntityTypeVersionLoadConfig config = versionLoadRequest.getEntityTypes().get(entityType);
EntityTypeVersionLoadConfig config = request.getEntityTypes().get(entityType);
AtomicInteger created = new AtomicInteger();
AtomicInteger updated = new AtomicInteger();
@ -275,29 +275,37 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
List<EntityExportData> entityDataList;
do {
try {
entityDataList = gitServiceQueue.getEntities(user.getTenantId(), versionLoadRequest.getVersionId(), entityType, offset, limit).get();
entityDataList = gitServiceQueue.getEntities(user.getTenantId(), request.getVersionId(), entityType, offset, limit).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
EntityImportSettings importSettings = EntityImportSettings.builder()
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.findExistingByName(config.isFindExistingEntityByName())
.build();
for (EntityExportData entityData : entityDataList) {
EntityImportResult<?> importResult;
try {
EntityImportResult<?> importResult = exportImportService.importEntity(user, entityData, EntityImportSettings.builder()
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.findExistingByName(config.isFindExistingEntityByName())
.build(), false, false);
if (importResult.getOldEntity() == null) created.incrementAndGet();
else updated.incrementAndGet();
saveReferencesCallbacks.add(importResult.getSaveReferencesCallback());
sendEventsCallbacks.add(importResult.getSendEventsCallback());
importResult = exportImportService.importEntity(user, entityData,
importSettings, false, false);
} catch (Exception e) {
throw new LoadEntityException(entityData, e);
}
if (importResult.getUpdatedAllExternalIds() != null && !importResult.getUpdatedAllExternalIds()) {
toReimport.put(entityData.getEntity().getExternalId(), importSettings);
continue;
}
if (importResult.getOldEntity() == null) created.incrementAndGet();
else updated.incrementAndGet();
saveReferencesCallbacks.add(importResult.getSaveReferencesCallback());
sendEventsCallbacks.add(importResult.getSendEventsCallback());
importedEntities.computeIfAbsent(entityType, t -> new HashSet<>())
.add(importResult.getSavedEntity().getId());
}
offset += limit;
importedEntities.computeIfAbsent(entityType, t -> new HashSet<>())
.addAll(entityDataList.stream().map(entityData -> entityData.getEntity().getExternalId()).collect(Collectors.toSet()));
} while (entityDataList.size() == limit);
results.put(entityType, EntityTypeLoadResult.builder()
.entityType(entityType)
@ -306,14 +314,33 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.build());
}
versionLoadRequest.getEntityTypes().keySet().stream()
.filter(entityType -> versionLoadRequest.getEntityTypes().get(entityType).isRemoveOtherEntities())
toReimport.forEach((externalId, importSettings) -> {
try {
EntityExportData entityData = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), externalId).get();
importSettings.setResetExternalIdsOfAnotherTenant(true);
EntityImportResult<?> importResult = exportImportService.importEntity(user, entityData,
importSettings, false, false);
EntityTypeLoadResult stats = results.get(externalId.getEntityType());
if (importResult.getOldEntity() == null) stats.setCreated(stats.getCreated() + 1);
else stats.setUpdated(stats.getUpdated() + 1);
saveReferencesCallbacks.add(importResult.getSaveReferencesCallback());
sendEventsCallbacks.add(importResult.getSendEventsCallback());
importedEntities.computeIfAbsent(externalId.getEntityType(), t -> new HashSet<>())
.add(importResult.getSavedEntity().getId());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
request.getEntityTypes().keySet().stream()
.filter(entityType -> request.getEntityTypes().get(entityType).isRemoveOtherEntities())
.sorted(exportImportService.getEntityTypeComparatorForImport().reversed())
.forEach(entityType -> {
DaoUtil.processInBatches(pageLink -> {
return exportableEntitiesService.findEntitiesByTenantId(user.getTenantId(), entityType, pageLink);
}, 100, entity -> {
if (entity.getExternalId() == null || !importedEntities.get(entityType).contains(entity.getExternalId())) {
if (!importedEntities.get(entityType).contains(entity.getId())) {
try {
exportableEntitiesService.checkPermission(user, entity, entityType, Operation.DELETE);
} catch (ThingsboardException e) {
@ -381,8 +408,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.exportAttributes(otherVersion.getAttributes() != null)
.build());
return transform(gitServiceQueue.getContentsDiff(user.getTenantId(),
JacksonUtil.toPrettyString(currentVersion.sort()),
JacksonUtil.toPrettyString(otherVersion.sort())),
JacksonUtil.toPrettyString(currentVersion.sort()),
JacksonUtil.toPrettyString(otherVersion.sort())),
rawDiff -> new EntityDataDiff(currentVersion, otherVersion, rawDiff), MoreExecutors.directExecutor());
}, MoreExecutors.directExecutor());
}

191
application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java

@ -31,11 +31,26 @@ import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -328,4 +343,180 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest {
return Futures.allAsList(futures);
}
@Test
public void testUpdateQueueConfigForIsolatedTenant() throws Exception {
Comparator<Queue> queueComparator = Comparator.comparing(Queue::getName);
final String username = "isolatedtenant@thingsboard.org";
final String password = "123456";
loginSysAdmin();
List<Queue> sysAdminQueues;
PageLink pageLink = new PageLink(10);
PageData<Queue> pageData;
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() {
}, pageLink);
sysAdminQueues = pageData.getData();
Tenant tenant = new Tenant();
tenant.setTitle("Isolated tenant");
tenant = doPost("/api/tenant", tenant, Tenant.class);
User tenantUser = new User();
tenantUser.setAuthority(Authority.TENANT_ADMIN);
tenantUser.setTenantId(tenant.getId());
tenantUser.setEmail(username);
createUserAndLogin(tenantUser, password);
List<Queue> foundTenantQueues;
pageLink = new PageLink(10);
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() {}, pageLink);
foundTenantQueues = pageData.getData();
Assert.assertEquals(sysAdminQueues, foundTenantQueues);
loginSysAdmin();
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("isolated-tb-rule-engine");
TenantProfileData tenantProfileData = new TenantProfileData();
tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration());
tenantProfile.setProfileData(tenantProfileData);
tenantProfile.setIsolatedTbRuleEngine(true);
addQueueConfig(tenantProfile, "Main");
addQueueConfig(tenantProfile, "Test");
tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
tenant.setTenantProfileId(tenantProfile.getId());
doPost("/api/tenant", tenant, Tenant.class);
login(username, password);
pageLink = new PageLink(10);
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() {}, pageLink);
foundTenantQueues = pageData.getData();
Assert.assertEquals(2, foundTenantQueues.size());
List<Queue> queuesFromConfig = getQueuesFromConfig(tenantProfile.getProfileData().getQueueConfiguration(), foundTenantQueues);
queuesFromConfig.sort(queueComparator);
foundTenantQueues.sort(queueComparator);
Assert.assertEquals(queuesFromConfig, foundTenantQueues);
loginSysAdmin();
TenantProfile tenantProfile2 = new TenantProfile();
tenantProfile2.setName("isolated-tb-rule-engine2");
TenantProfileData tenantProfileData2 = new TenantProfileData();
tenantProfileData2.setConfiguration(new DefaultTenantProfileConfiguration());
tenantProfile2.setProfileData(tenantProfileData2);
tenantProfile2.setIsolatedTbRuleEngine(true);
addQueueConfig(tenantProfile2, "Main");
addQueueConfig(tenantProfile2, "Test");
addQueueConfig(tenantProfile2, "Test2");
tenantProfile2 = doPost("/api/tenantProfile", tenantProfile2, TenantProfile.class);
tenant.setTenantProfileId(tenantProfile2.getId());
doPost("/api/tenant", tenant, Tenant.class);
login(username, password);
pageLink = new PageLink(10);
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() {}, pageLink);
foundTenantQueues = pageData.getData();
Assert.assertEquals(3, foundTenantQueues.size());
queuesFromConfig = getQueuesFromConfig(tenantProfile2.getProfileData().getQueueConfiguration(), foundTenantQueues);
queuesFromConfig.sort(queueComparator);
foundTenantQueues.sort(queueComparator);
Assert.assertEquals(queuesFromConfig, foundTenantQueues);
loginSysAdmin();
tenantProfile2.getProfileData().getQueueConfiguration().removeIf(q -> q.getName().equals("Test"));
tenantProfile2.getProfileData().getQueueConfiguration().removeIf(q -> q.getName().equals("Test2"));
addQueueConfig(tenantProfile2, "Test2");
addQueueConfig(tenantProfile2, "Test3");
tenantProfile2 = doPost("/api/tenantProfile", tenantProfile2, TenantProfile.class);
login(username, password);
pageLink = new PageLink(10);
pageData = doGetTypedWithPageLink("/api/queues?serviceType=TB_RULE_ENGINE&", new TypeReference<>() {}, pageLink);
foundTenantQueues = pageData.getData();
Assert.assertEquals(3, foundTenantQueues.size());
queuesFromConfig = getQueuesFromConfig(tenantProfile2.getProfileData().getQueueConfiguration(), foundTenantQueues);
queuesFromConfig.sort(queueComparator);
foundTenantQueues.sort(queueComparator);
Assert.assertEquals(queuesFromConfig, foundTenantQueues);
loginSysAdmin();
tenant.setTenantProfileId(null);
doPost("/api/tenant", tenant, Tenant.class);
login(username, password);
for (Queue queue : foundTenantQueues) {
doGet("/api/queues/" + queue.getId()).andExpect(status().isNotFound());
}
loginSysAdmin();
doDelete("/api/tenant/" + tenant.getId().getId().toString()).andExpect(status().isOk());
}
private void addQueueConfig(TenantProfile tenantProfile, String queueName) {
TenantProfileQueueConfiguration queueConfiguration = new TenantProfileQueueConfiguration();
queueConfiguration.setName(queueName);
queueConfiguration.setTopic("tb_rule_engine." + queueName.toLowerCase());
queueConfiguration.setPollInterval(25);
queueConfiguration.setPartitions(new Random().nextInt(100));
queueConfiguration.setConsumerPerPartition(true);
queueConfiguration.setPackProcessingTimeout(2000);
SubmitStrategy submitStrategy = new SubmitStrategy();
submitStrategy.setType(SubmitStrategyType.BURST);
submitStrategy.setBatchSize(1000);
queueConfiguration.setSubmitStrategy(submitStrategy);
ProcessingStrategy processingStrategy = new ProcessingStrategy();
processingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES);
processingStrategy.setRetries(3);
processingStrategy.setFailurePercentage(0);
processingStrategy.setPauseBetweenRetries(3);
processingStrategy.setMaxPauseBetweenRetries(3);
queueConfiguration.setProcessingStrategy(processingStrategy);
TenantProfileData profileData = tenantProfile.getProfileData();
List<TenantProfileQueueConfiguration> configs = profileData.getQueueConfiguration();
if (configs == null) {
configs = new ArrayList<>();
}
configs.add(queueConfiguration);
profileData.setQueueConfiguration(configs);
tenantProfile.setProfileData(profileData);
}
private List<Queue> getQueuesFromConfig(List<TenantProfileQueueConfiguration> queueConfiguration, List<Queue> queues) {
List<Queue> result = new ArrayList<>();
Map<String, Queue> queueMap = new HashMap<>();
for (Queue queue : queues) {
queueMap.put(queue.getName(), queue);
}
for (TenantProfileQueueConfiguration config : queueConfiguration) {
Queue queue = queueMap.get(config.getName());
if (queue != null) {
Queue expectedQueue = new Queue(queue.getTenantId(), config);
expectedQueue.setId(queue.getId());
expectedQueue.setCreatedTime(queue.getCreatedTime());
result.add(queue);
}
}
return result;
}
}

1
common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java

@ -52,6 +52,7 @@ public class Queue extends SearchTextBasedWithAdditionalInfo<QueueId> implements
this.packProcessingTimeout = queueConfiguration.getPackProcessingTimeout();
this.submitStrategy = queueConfiguration.getSubmitStrategy();
this.processingStrategy = queueConfiguration.getProcessingStrategy();
setAdditionalInfo(queueConfiguration.getAdditionalInfo());
}
@Override

2
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportResult.java

@ -31,6 +31,8 @@ public class EntityImportResult<E extends ExportableEntity<? extends EntityId>>
private ThrowingRunnable saveReferencesCallback = () -> {};
private ThrowingRunnable sendEventsCallback = () -> {};
private Boolean updatedAllExternalIds;
public void addSaveReferencesCallback(ThrowingRunnable callback) {
this.saveReferencesCallback = this.saveReferencesCallback.andThen(callback);
}

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java

@ -29,4 +29,7 @@ public class EntityImportSettings {
private boolean updateRelations;
private boolean saveAttributes;
private boolean saveCredentials;
// internal
private boolean resetExternalIdsOfAnotherTenant;
}

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

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.common.data.tenant.profile;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -46,6 +44,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String transportDeviceTelemetryMsgRateLimit;
private String transportDeviceTelemetryDataPointsRateLimit;
private String tenantEntityExportRateLimit;
private String tenantEntityImportRateLimit;
private long maxTransportMessages;
private long maxTransportDataPoints;
private long maxREExecutions;

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.tenant.profile;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
@ -29,4 +30,5 @@ public class TenantProfileQueueConfiguration {
private long packProcessingTimeout;
private SubmitStrategy submitStrategy;
private ProcessingStrategy processingStrategy;
private JsonNode additionalInfo;
}

8
common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java

@ -27,6 +27,7 @@ import java.time.Duration;
*/
public class TbRateLimits {
private final LocalBucket bucket;
private final String configuration;
public TbRateLimits(String limitsConfiguration) {
LocalBucketBuilder builder = Bucket4j.builder();
@ -42,8 +43,7 @@ public class TbRateLimits {
} else {
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
}
this.configuration = limitsConfiguration;
}
public boolean tryConsume() {
@ -54,4 +54,8 @@ public class TbRateLimits {
return bucket.tryConsume(number);
}
public String getConfiguration() {
return configuration;
}
}

6
dao/src/main/java/org/thingsboard/server/dao/service/validator/QueueValidator.java

@ -40,12 +40,12 @@ public class QueueValidator extends DataValidator<Queue> {
@Override
protected void validateCreate(TenantId tenantId, Queue queue) {
if (queueDao.findQueueByTenantIdAndTopic(tenantId, queue.getTopic()) != null) {
throw new DataValidationException(String.format("Queue with topic: %s already exists!", queue.getTopic()));
}
if (queueDao.findQueueByTenantIdAndName(tenantId, queue.getName()) != null) {
throw new DataValidationException(String.format("Queue with name: %s already exists!", queue.getName()));
}
if (queueDao.findQueueByTenantIdAndTopic(tenantId, queue.getTopic()) != null) {
throw new DataValidationException(String.format("Queue with topic: %s already exists!", queue.getTopic()));
}
}
@Override

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.node.NullNode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
@ -73,6 +74,7 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest {
mainQueueProcessingStrategy.setPauseBetweenRetries(3);
mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3);
mainQueueConfiguration.setProcessingStrategy(mainQueueProcessingStrategy);
mainQueueConfiguration.setAdditionalInfo(NullNode.getInstance());
tenantProfile.getProfileData().setQueueConfiguration(Collections.singletonList(mainQueueConfiguration));
TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);

100
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -37,7 +37,6 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rest.client.utils.RestJsonConverter;
import org.thingsboard.server.common.data.AdminSettings;
@ -91,6 +90,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -119,6 +119,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
@ -146,7 +147,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
@ -328,11 +328,11 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString());
params.put("fetchOriginator", String.valueOf(fetchOriginator));
if(searchStatus != null) {
if (searchStatus != null) {
params.put("searchStatus", searchStatus.name());
urlSecondPart += "&searchStatus={searchStatus}";
}
if(status != null) {
if (status != null) {
params.put("status", status.name());
urlSecondPart += "&status={status}";
}
@ -340,7 +340,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
@ -523,12 +523,12 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public List<Asset> getAssetsByIds(List<AssetId> assetIds) {
return restTemplate.exchange(
baseURL + "/api/assets?assetIds={assetIds}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<Asset>>() {
},
listIdsToString(assetIds))
baseURL + "/api/assets?assetIds={assetIds}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<Asset>>() {
},
listIdsToString(assetIds))
.getBody();
}
@ -543,7 +543,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public List<EntitySubtype> getAssetTypes() {
return restTemplate.exchange(URI.create(
baseURL + "/api/asset/types"),
baseURL + "/api/asset/types"),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<EntitySubtype>>() {
@ -746,13 +746,13 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public List<ComponentDescriptor> getComponentDescriptorsByTypes(List<ComponentType> componentTypes, RuleChainType ruleChainType) {
return restTemplate.exchange(
baseURL + "/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<ComponentDescriptor>>() {
},
listEnumToString(componentTypes),
ruleChainType)
baseURL + "/api/components?componentTypes={componentTypes}&ruleChainType={ruleChainType}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<ComponentDescriptor>>() {
},
listEnumToString(componentTypes),
ruleChainType)
.getBody();
}
@ -2904,7 +2904,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/resource/{resourceId}/download",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {
},
params
);
}
@ -2917,7 +2918,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/resource/info/{resourceId}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<TbResourceInfo>() {},
new ParameterizedTypeReference<TbResourceInfo>() {
},
params
).getBody();
}
@ -2930,7 +2932,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/resource/{resourceId}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<TbResource>() {},
new ParameterizedTypeReference<TbResource>() {
},
params
).getBody();
}
@ -2950,7 +2953,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/resource?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<TbResourceInfo>>() {},
new ParameterizedTypeReference<PageData<TbResourceInfo>>() {
},
params
).getBody();
}
@ -2967,7 +2971,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/otaPackage/{otaPackageId}/download",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {
},
params
);
}
@ -2980,7 +2985,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/otaPackage/info/{otaPackageId}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<OtaPackageInfo>() {},
new ParameterizedTypeReference<OtaPackageInfo>() {
},
params
).getBody();
}
@ -2993,7 +2999,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
baseURL + "/api/otaPackage/{otaPackageId}",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<OtaPackage>() {},
new ParameterizedTypeReference<OtaPackage>() {
},
params
).getBody();
}
@ -3004,13 +3011,13 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return restTemplate.postForEntity(baseURL + "/api/otaPackage?isUrl={isUrl}", otaPackageInfo, OtaPackageInfo.class, params).getBody();
}
public OtaPackageInfo saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception {
public OtaPackageInfo saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, String fileName, byte[] fileBytes) throws Exception {
HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, String> fileMap = new LinkedMultiValueMap<>();
fileMap.add(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=file; filename=" + file.getName());
HttpEntity<ByteArrayResource> fileEntity = new HttpEntity<>(new ByteArrayResource(file.getBytes()), fileMap);
fileMap.add(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=file; filename=" + fileName);
HttpEntity<ByteArrayResource> fileEntity = new HttpEntity<>(new ByteArrayResource(fileBytes), fileMap);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", fileEntity);
@ -3021,7 +3028,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
params.put("checksumAlgorithm", checksumAlgorithm.name());
String url = "/api/otaPackage/{otaPackageId}?checksumAlgorithm={checksumAlgorithm}";
if(checkSum != null) {
if (checkSum != null) {
url += "&checkSum={checkSum}";
}
@ -3068,6 +3075,39 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
restTemplate.delete(baseURL + "/api/otaPackage/{otaPackageId}", otaPackageId.getId().toString());
}
public PageData<Queue> getQueuesByServiceType(String serviceType, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("serviceType", serviceType);
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/queues?{serviceType}&" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<Queue>>() {
},
params
).getBody();
}
public Queue getQueueById(QueueId queueId) {
return restTemplate.exchange(
baseURL + "/api/queue/" + queueId,
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<Queue>() {
}
).getBody();
}
public Queue saveQueue(Queue queue, String serviceType) {
return restTemplate.postForEntity(baseURL + "/api/queues?serviceType=" + serviceType, queue, Queue.class).getBody();
}
public void deleteQueue(QueueId queueId) {
restTemplate.delete(baseURL + "/api/queues/" + queueId);
}
@Deprecated
public Optional<JsonNode> getAttributes(String accessToken, String clientKeys, String sharedKeys) {
Map<String, String> params = new HashMap<>();

37
ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.html

@ -16,33 +16,16 @@
-->
<div fxLayout="column">
<div class="tb-tenant-profile-queues">
<mat-accordion multi>
<mat-expansion-panel fxFlex expanded
*ngFor="let queuesControl of queuesFormArray.controls; trackBy: trackByQueue;
let $index = index; last as isLast;">
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title>
{{ getName(queuesControl.value.name) }}
</mat-panel-title>
<span fxFlex></span>
<button *ngIf="!($index === 0) && !this.disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeQueue($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-queue-form [formControl]="queuesControl"
[newQueue]="newQueue">
</tb-queue-form>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
<div class="tb-tenant-profile-queues"
*ngFor="let queuesControl of queuesFormArray.controls; trackBy: trackByQueue;
let $index = index; last as isLast;"
[ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-queue-form [formControl]="queuesControl"
(removeQueue)="removeQueue($index)"
[mainQueue]="$index === 0"
[expanded]="$index === 0"
[newQueue]="newQueue">
</tb-queue-form>
</div>
<div *ngIf="!queuesFormArray.controls.length">
<span translate fxLayoutAlign="center center"

12
ui-ngx/src/app/modules/home/components/profile/queue/tenant-profile-queues.component.ts

@ -33,6 +33,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Subscription } from 'rxjs';
import { QueueInfo } from '@shared/models/queue.models';
import { UtilsService } from '@core/services/utils.service';
import { guid } from '@core/utils';
@Component({
selector: 'tb-tenant-profile-queues',
@ -131,7 +132,10 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
}
public trackByQueue(index: number, queueControl: AbstractControl) {
return queueControl;
if (queueControl) {
return queueControl.value.id;
}
return null;
}
public removeQueue(index: number) {
@ -140,6 +144,7 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
public addQueue() {
const queue = {
id: guid(),
consumerPerPartition: false,
name: '',
packProcessingTimeout: 2000,
@ -156,7 +161,10 @@ export class TenantProfileQueuesComponent implements ControlValueAccessor, Valid
batchSize: 0,
type: ''
},
topic: ''
topic: '',
additionalInfo: {
description: ''
}
};
this.newQueue = true;
const queuesArray = this.tenantProfileQueuesFormGroup.get('queues') as FormArray;

16
ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts

@ -14,12 +14,13 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TenantProfileData } from '@shared/models/tenant.model';
import { Subscription } from 'rxjs';
@Component({
selector: 'tb-tenant-profile-data',
@ -31,7 +32,7 @@ import { TenantProfileData } from '@shared/models/tenant.model';
multi: true
}]
})
export class TenantProfileDataComponent implements ControlValueAccessor, OnInit {
export class TenantProfileDataComponent implements ControlValueAccessor, OnInit, OnDestroy {
tenantProfileDataFormGroup: FormGroup;
@ -47,6 +48,7 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
private valueChange$: Subscription = null;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -64,11 +66,17 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit
this.tenantProfileDataFormGroup = this.fb.group({
configuration: [null, Validators.required]
});
this.tenantProfileDataFormGroup.valueChanges.subscribe(() => {
this.valueChange$ = this.tenantProfileDataFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -87,7 +95,7 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit
if (this.tenantProfileDataFormGroup.valid) {
tenantProfileData = this.tenantProfileDataFormGroup.getRawValue();
}
this.propagateChange(tenantProfileData);
this.propagateChange(tenantProfileData.configuration);
}
}

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

@ -35,6 +35,10 @@
width: fit-content;
}
}
.mat-expansion-panel-header {
height: 48px;
}
.expansion-panel-block {
padding-bottom: 16px;
}

7
ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts

@ -23,6 +23,7 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { EntityComponent } from '../entity/entity.component';
import { guid } from '@core/utils';
@Component({
selector: 'tb-tenant-profile',
@ -54,6 +55,7 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
buildForm(entity: TenantProfile): FormGroup {
const mainQueue = [
{
id: guid(),
consumerPerPartition: true,
name: 'Main',
packProcessingTimeout: 2000,
@ -70,7 +72,10 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> {
batchSize: 1000,
type: 'BURST'
},
topic: 'tb_rule_engine.main'
topic: 'tb_rule_engine.main',
additionalInfo: {
description: ''
}
}
];
const formGroup = this.fb.group(

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

@ -280,4 +280,12 @@
<mat-label translate>tenant-profile.transport-device-telemetry-data-points-rate-limit</mat-label>
<input matInput formControlName="transportDeviceTelemetryDataPointsRateLimit">
</mat-form-field>
<mat-form-field 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 class="mat-block">
<mat-label translate>tenant-profile.tenant-entity-import-rate-limit</mat-label>
<input matInput formControlName="tenantEntityImportRateLimit">
</mat-form-field>
</section>

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

@ -67,6 +67,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
transportDeviceMsgRateLimit: [null, []],
transportDeviceTelemetryMsgRateLimit: [null, []],
transportDeviceTelemetryDataPointsRateLimit: [null, []],
tenantEntityExportRateLimit: [null, []],
tenantEntityImportRateLimit: [null, []],
maxTransportMessages: [null, [Validators.required, Validators.min(0)]],
maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]],
maxREExecutions: [null, [Validators.required, Validators.min(0)]],

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

@ -15,161 +15,182 @@
limitations under the License.
-->
<form [formGroup]="queueFormGroup" fxLayout="column" fxLayoutGap="0.5em">
<mat-form-field class="mat-block">
<mat-label translate>admin.queue-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="queueFormGroup.get('name').hasError('required')">
{{ 'queue.name-required' | translate }}
</mat-error>
</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-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title translate>
queue.submit-strategy
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div formGroupName="submitStrategy">
<mat-form-field class="mat-block">
<mat-label translate>queue.submit-strategy</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let strategy of submitStrategies" [value]="strategy">
{{ strategy }}
</mat-option>
</mat-select>
<mat-error *ngIf="queueFormGroup.get('submitStrategy.type').hasError('required')">
{{ 'queue.submit-strategy-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" *ngIf="hideBatchSize">
<mat-label translate>queue.batch-size</mat-label>
<input type="number" matInput formControlName="batchSize" required>
<mat-error *ngIf="queueFormGroup.get('submitStrategy.batchSize').hasError('required')">
{{ 'queue.batch-size-required' | translate }}
</mat-error>
<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>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title translate>
queue.processing-strategy
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div formGroupName="processingStrategy">
<mat-form-field class="mat-block">
<mat-label translate>queue.processing-strategy</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let strategy of processingStrategies" [value]="strategy">
{{ strategy }}
</mat-option>
</mat-select>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.type').hasError('required')">
{{ 'queue.processing-strategy-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.retries</mat-label>
<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')">
{{ 'queue.retries-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.failure-percentage</mat-label>
<input type="number" matInput formControlName="failurePercentage" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('required')">
{{ 'queue.failure-percentage-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('min') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('max')">
{{ 'queue.failure-percentage-min-value' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('max') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('min')">
{{ 'queue.failure-percentage-max-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.pause-between-retries</mat-label>
<input type="number" matInput formControlName="pauseBetweenRetries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')">
{{ 'queue.pause-between-retries-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('min') &&
!queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')">
{{ 'queue.pause-between-retries-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.max-pause-between-retries</mat-label>
<input type="number" matInput formControlName="maxPauseBetweenRetries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')">
{{ 'queue.max-pause-between-retries-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('min') &&
!queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')">
{{ 'queue.max-pause-between-retries-min-value' | translate }}
</mat-error>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
<mat-form-field class="mat-block" formGroupName="additionalInfo">
<mat-label translate>queue.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</form>
<mat-expansion-panel fxFlex [(expanded)]="expanded">
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title>
{{ queueTitle }}
</mat-panel-title>
<span fxFlex></span>
<button *ngIf="!mainQueue && !disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeQueue.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<form [formGroup]="queueFormGroup" fxLayout="column" fxLayoutGap="0.5em">
<mat-form-field class="mat-block">
<mat-label translate>admin.queue-name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="queueFormGroup.get('name').hasError('required')">
{{ 'queue.name-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('name').hasError('unique')">
{{ 'queue.name-unique' | translate }}
</mat-error>
</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-expansion-panel [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title translate>
queue.submit-strategy
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div formGroupName="submitStrategy">
<mat-form-field class="mat-block">
<mat-label translate>queue.submit-strategy</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let strategy of submitStrategies" [value]="strategy">
{{ strategy }}
</mat-option>
</mat-select>
<mat-error *ngIf="queueFormGroup.get('submitStrategy.type').hasError('required')">
{{ 'queue.submit-strategy-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" *ngIf="hideBatchSize">
<mat-label translate>queue.batch-size</mat-label>
<input type="number" matInput formControlName="batchSize" required>
<mat-error *ngIf="queueFormGroup.get('submitStrategy.batchSize').hasError('required')">
{{ 'queue.batch-size-required' | translate }}
</mat-error>
<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>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title translate>
queue.processing-strategy
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div formGroupName="processingStrategy">
<mat-form-field class="mat-block">
<mat-label translate>queue.processing-strategy</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let strategy of processingStrategies" [value]="strategy">
{{ strategy }}
</mat-option>
</mat-select>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.type').hasError('required')">
{{ 'queue.processing-strategy-type-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.retries</mat-label>
<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')">
{{ 'queue.retries-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.failure-percentage</mat-label>
<input type="number" matInput formControlName="failurePercentage" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('required')">
{{ 'queue.failure-percentage-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('min') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('max')">
{{ 'queue.failure-percentage-min-value' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.failurePercentage').hasError('max') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('required') &&
!queueFormGroup.get('processingStrategy.failurePercentage').hasError('min')">
{{ 'queue.failure-percentage-max-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.pause-between-retries</mat-label>
<input type="number" matInput formControlName="pauseBetweenRetries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')">
{{ 'queue.pause-between-retries-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('min') &&
!queueFormGroup.get('processingStrategy.pauseBetweenRetries').hasError('required')">
{{ 'queue.pause-between-retries-min-value' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>queue.max-pause-between-retries</mat-label>
<input type="number" matInput formControlName="maxPauseBetweenRetries" required>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')">
{{ 'queue.max-pause-between-retries-required' | translate }}
</mat-error>
<mat-error *ngIf="queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('min') &&
!queueFormGroup.get('processingStrategy.maxPauseBetweenRetries').hasError('required')">
{{ 'queue.max-pause-between-retries-min-value' | translate }}
</mat-error>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
<mat-form-field class="mat-block" formGroupName="additionalInfo">
<mat-label translate>queue.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</form>
</ng-template>
</mat-expansion-panel>

37
ui-ngx/src/app/modules/home/components/queue/queue-form.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -29,6 +29,7 @@ import { MatDialog } from '@angular/material/dialog';
import { UtilsService } from '@core/services/utils.service';
import { QueueInfo, QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models';
import { isDefinedAndNotNull } from '@core/utils';
import { Subscription } from 'rxjs';
@Component({
selector: 'tb-queue-form',
@ -47,7 +48,7 @@ import { isDefinedAndNotNull } from '@core/utils';
}
]
})
export class QueueFormComponent implements ControlValueAccessor, OnInit, Validator {
export class QueueFormComponent implements ControlValueAccessor, OnInit, OnDestroy, Validator {
@Input()
disabled: boolean;
@ -55,20 +56,28 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, Validat
@Input()
newQueue = false;
@Input()
mainQueue = false;
@Input()
systemQueue = false;
private modelValue: QueueInfo;
@Input()
expanded = false;
queueFormGroup: FormGroup;
@Output()
removeQueue = new EventEmitter();
queueFormGroup: FormGroup;
submitStrategies: string[] = [];
processingStrategies: string[] = [];
queueTitle = '';
hideBatchSize = false;
private modelValue: QueueInfo;
private propagateChange = null;
private propagateChangePending = false;
private valueChange$: Subscription = null;
constructor(private dialog: MatDialog,
private utils: UtilsService,
@ -114,10 +123,13 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, Validat
description: ['']
})
});
this.queueFormGroup.valueChanges.subscribe(() => {
this.valueChange$ = this.queueFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.queueFormGroup.get('name').valueChanges.subscribe((value) => this.queueFormGroup.patchValue({topic: `tb_rule_engine.${value}`}));
this.queueFormGroup.get('name').valueChanges.subscribe((value) => {
this.queueFormGroup.patchValue({topic: `tb_rule_engine.${value}`});
this.queueTitle = this.utils.customTranslation(value, value);
});
this.queueFormGroup.get('submitStrategy').get('type').valueChanges.subscribe(() => {
this.submitStrategyTypeChanged();
});
@ -128,6 +140,13 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, Validat
}
}
ngOnDestroy() {
if (this.valueChange$) {
this.valueChange$.unsubscribe();
this.valueChange$ = null;
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -141,6 +160,10 @@ export class QueueFormComponent implements ControlValueAccessor, OnInit, Validat
writeValue(value: QueueInfo): void {
this.propagateChangePending = false;
this.modelValue = value;
if (!this.modelValue.name) {
this.expanded = true;
}
this.queueTitle = this.utils.customTranslation(value.name, value.name);
if (isDefinedAndNotNull(this.modelValue)) {
this.queueFormGroup.patchValue(this.modelValue, {emitEvent: false});
}

18
ui-ngx/src/app/modules/home/pages/tenant-profile/tenant-profiles-table-config.resolver.ts

@ -33,6 +33,8 @@ import { TenantProfileComponent } from '../../components/profile/tenant-profile.
import { TenantProfileTabsComponent } from './tenant-profile-tabs.component';
import { DialogService } from '@core/services/dialog.service';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { map } from 'rxjs/operators';
import { guid } from '@core/utils';
@Injectable()
export class TenantProfilesTableConfigResolver implements Resolve<EntityTableConfig<TenantProfile>> {
@ -84,7 +86,12 @@ export class TenantProfilesTableConfigResolver implements Resolve<EntityTableCon
this.config.deleteEntitiesContent = () => this.translate.instant('tenant-profile.delete-tenant-profiles-text');
this.config.entitiesFetchFunction = pageLink => this.tenantProfileService.getTenantProfiles(pageLink);
this.config.loadEntity = id => this.tenantProfileService.getTenantProfile(id.id);
this.config.loadEntity = id => this.tenantProfileService.getTenantProfile(id.id).pipe(
map(tenantProfile => ({
...tenantProfile,
profileData: {...tenantProfile.profileData, queueConfiguration: this.addId(tenantProfile.profileData.queueConfiguration)},
}))
);
this.config.saveEntity = tenantProfile => this.tenantProfileService.saveTenantProfile(tenantProfile);
this.config.deleteEntity = id => this.tenantProfileService.deleteTenantProfile(id.id);
this.config.onEntityAction = action => this.onTenantProfileAction(action);
@ -93,6 +100,15 @@ export class TenantProfilesTableConfigResolver implements Resolve<EntityTableCon
this.config.addActionDescriptors = this.configureAddActions();
}
addId(queues) {
const queuesWithId = [];
queues.forEach(value => {
value.id = guid();
queuesWithId.push(value);
});
return queuesWithId;
}
resolve(): EntityTableConfig<TenantProfile> {
this.config.tableTitle = this.translate.instant('tenant-profile.tenant-profiles');

9
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectQueueFormGroup" class="mat-block">
<mat-form-field [formGroup]="selectQueueFormGroup" class="mat-block autocomplete-queue">
<input matInput type="text" placeholder="{{ 'queue.queue-name' | translate }}"
#queueInput
formControlName="queueId"
@ -30,10 +30,11 @@
</button>
<mat-autocomplete class="tb-autocomplete"
#queueAutocomplete="matAutocomplete"
[displayWith]="displayQueueFn">
<mat-option *ngFor="let queue of filteredQueues | async" [value]="queue">
[displayWith]="displayQueueFn"
>
<mat-option *ngFor="let queue of filteredQueues | async" [value]="queue" class="queue-option">
<span [innerHTML]="queue.name | highlight:searchText"></span>
<small style="display: block;">{{getDescription(queue)}}</small>
<small class="queue-option-description">{{getDescription(queue)}}</small>
</mat-option>
<mat-option *ngIf="!(filteredQueues | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">

29
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.scss

@ -0,0 +1,29 @@
/**
* 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.
*/
::ng-deep {
.queue-option {
.mat-option-text {
display: inline;
}
.queue-option-description {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

7
ui-ngx/src/app/shared/components/queue/queue-autocomplete.component.ts

@ -36,7 +36,7 @@ import { emptyPageData } from '@shared/models/page/page-data';
@Component({
selector: 'tb-queue-autocomplete',
templateUrl: './queue-autocomplete.component.html',
styleUrls: [],
styleUrls: ['./queue-autocomplete.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QueueAutocompleteComponent),
@ -207,7 +207,10 @@ export class QueueAutocompleteComponent implements ControlValueAccessor, OnInit
getDescription(value) {
return value.additionalInfo?.description ? value.additionalInfo.description :
`Submit Strategy: ${value.submitStrategy.type}, Processing Strategy: ${value.processingStrategy.type}`;
this.translate.instant(
'queue.alt-description',
{submitStrategy: value.submitStrategy.type, processingStrategy: value.processingStrategy.type}
);
}
clear() {

1
ui-ngx/src/app/shared/models/queue.models.ts

@ -43,6 +43,7 @@ export enum QueueProcessingStrategyTypes {
}
export interface QueueInfo extends BaseData<QueueId> {
generatedId?: string;
name: string;
packProcessingTimeout: number;
partitions: number;

5
ui-ngx/src/app/shared/models/tenant.model.ts

@ -41,6 +41,9 @@ export interface DefaultTenantProfileConfiguration {
transportDeviceTelemetryMsgRateLimit?: string;
transportDeviceTelemetryDataPointsRateLimit?: string;
tenantEntityExportRateLimit?: string;
tenantEntityImportRateLimit?: string;
maxTransportMessages: number;
maxTransportDataPoints: number;
maxREExecutions: number;
@ -98,7 +101,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
export interface TenantProfileData {
configuration: TenantProfileConfiguration;
queueConfiguration?: QueueInfo;
queueConfiguration?: Array<QueueInfo>;
}
export interface TenantProfile extends BaseData<TenantProfileId> {

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

@ -2799,6 +2799,7 @@
"select-name": "Select queue name",
"name": "Name",
"name-required": "Queue name is required!",
"name-unique": "Queue name is not unique!",
"queue-required": "Queue is required!",
"topic-required": "Queue topic is required!",
"poll-interval-required": "Poll interval is required!",
@ -2845,7 +2846,8 @@
"delete": "Delete queue",
"copyId": "Copy queue Id",
"idCopiedMessage": "Queue Id has been copied to clipboard",
"description": "Description"
"description": "Description",
"alt-description": "Submit Strategy: {{submitStrategy}}, Processing Strategy: {{processingStrategy}}"
},
"tenant": {
"tenant": "Tenant",
@ -2947,6 +2949,8 @@
"transport-device-msg-rate-limit": "Transport device messages rate limit.",
"transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages rate limit.",
"transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points rate limit.",
"tenant-entity-export-rate-limit": "Entity version creation rate limit",
"tenant-entity-import-rate-limit": "Entity version load rate limit",
"max-transport-messages": "Maximum number of transport messages (0 - unlimited)",
"max-transport-messages-required": "Maximum number of transport messages is required.",
"max-transport-messages-range": "Maximum number of transport messages can't be negative",

Loading…
Cancel
Save